diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 00000000000..36643c6062c --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "public/vendor/" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5988579123a..c046d3364cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,27 @@ # 2.1.0 (unreleased - master branch) +**Data sources** +- [Issue #1525](https://github.com/grafana/grafana/issues/1525). InfluxDB: Full support for InfluxDB 0.9 with new adapted query editor +- [Issue #2191](https://github.com/grafana/grafana/issues/2191). KariosDB: Grafana now ships with a KariosDB data source plugin, thx @masaori335 +- [Issue #1177](https://github.com/grafana/grafana/issues/1177). OpenTSDB: Limit tags by metric, OpenTSDB config option tsd.core.meta.enable_realtime_ts must enabled for OpenTSDB lookup api +- [Issue #1250](https://github.com/grafana/grafana/issues/1250). OpenTSDB: Support for template variable values lookup queries + **New dashboard features** - [Issue #1144](https://github.com/grafana/grafana/issues/1144). Templating: You can now select multiple template variables values at the same time. - [Issue #1922](https://github.com/grafana/grafana/issues/1922). Templating: Specify multiple variable values via URL params. - [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value - [Issue #1888](https://github.com/grafana/grafana/issues/1944). Dashboard: Custom Navigation links & dynamic links to related dashboards - [Issue #590](https://github.com/grafana/grafana/issues/590). Graph: Define series color using regex rule +- [Issue #2162](https://github.com/grafana/grafana/issues/2162). Graph: New series style override, negative-y transform and stack groups - [Issue #2096](https://github.com/grafana/grafana/issues/2096). Dashboard list panel: Now supports search by multiple tags +- [Issue #2203](https://github.com/grafana/grafana/issues/2203). Singlestat: Now support string values **User or Organization admin** - [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user). - [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior **Backend** +- [Issue #2218](https://github.com/grafana/grafana/issues/2218). Auth: You can now authenicate against api with username / password using basic auth - [Issue #2095](https://github.com/grafana/grafana/issues/2095). Search: Search now supports filtering by multiple dashboard tags - [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski - [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj @@ -27,6 +36,10 @@ - Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI - Datasource HTTP api breaking change, ADD datasource is now POST /api/datasources/, update is now PUT /api/datasources/:id +**Fixes** +- [Issue #2185](https://github.com/grafana/grafana/issues/2185). Graph: fixed PNG rendering of panels with legend table to the right +- [Issue #2163](https://github.com/grafana/grafana/issues/2163). Backend: Load dashboards with capital letters in the dashboard url slug (url id) + # 2.0.3 (unreleased - 2.0.x branch) **Fixes** diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index dd08ac91121..063f0efedd7 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,6 @@ { "ImportPath": "github.com/grafana/grafana", - "GoVersion": "go1.3", + "GoVersion": "go1.4.2", "Packages": [ "./pkg/..." ], @@ -13,14 +13,6 @@ "ImportPath": "github.com/Unknwon/macaron", "Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a" }, - { - "ImportPath": "github.com/dalu/slug", - "Rev": "6dbd13912e9be466e2c1de349a2c7d1466c97e07" - }, - { - "ImportPath": "github.com/dalu/unidecode", - "Rev": "339814d47f3e32a6f7036a0a4c56ed9b373dd755" - }, { "ImportPath": "github.com/go-sql-driver/mysql", "Comment": "v1.2-26-g9543750", @@ -35,6 +27,10 @@ "Comment": "v0.4.2-58-ge2889e5", "Rev": "e2889e5517600b82905f1d2ba8b70deb71823ffe" }, + { + "ImportPath": "github.com/gosimple/slug", + "Rev": "8d258463b4459f161f51d6a357edacd3eef9d663" + }, { "ImportPath": "github.com/jtolds/gls", "Rev": "f1ac7f4f24f50328e6bc838ca4437d1612a0243c" @@ -56,6 +52,10 @@ "ImportPath": "github.com/mattn/go-sqlite3", "Rev": "e28cd440fabdd39b9520344bc26829f61db40ece" }, + { + "ImportPath": "github.com/rainycape/unidecode", + "Rev": "836ef0a715aedf08a12d595ed73ec8ed5b288cac" + }, { "ImportPath": "github.com/smartystreets/goconvey/convey", "Comment": "1.5.0-356-gfbc0a1c", @@ -87,10 +87,6 @@ "ImportPath": "gopkg.in/redis.v2", "Comment": "v2.3.2", "Rev": "e6179049628164864e6e84e973cfb56335748dea" - }, - { - "ImportPath": "gopkgs.com/pool.v1", - "Rev": "c850f092aad1780cbffff25f471c5cc32097932a" } ] } diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/README.md b/Godeps/_workspace/src/github.com/dalu/unidecode/README.md deleted file mode 100644 index 589d955593c..00000000000 --- a/Godeps/_workspace/src/github.com/dalu/unidecode/README.md +++ /dev/null @@ -1,6 +0,0 @@ -unidecode -========= - -Unicode transliterator in Golang - Replaces non-ASCII characters with their ASCII approximations. - -View other available versions, documentation and examples at http://gopkgs.com/unidecode diff --git a/Godeps/_workspace/src/github.com/gosimple/slug/.gitignore b/Godeps/_workspace/src/github.com/gosimple/slug/.gitignore new file mode 100644 index 00000000000..02a8da53752 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gosimple/slug/.gitignore @@ -0,0 +1,2 @@ +_* +cover*.out diff --git a/Godeps/_workspace/src/github.com/dalu/slug/README.md b/Godeps/_workspace/src/github.com/gosimple/slug/README.md similarity index 72% rename from Godeps/_workspace/src/github.com/dalu/slug/README.md rename to Godeps/_workspace/src/github.com/gosimple/slug/README.md index ddefc36ff04..a2649bdb170 100644 --- a/Godeps/_workspace/src/github.com/dalu/slug/README.md +++ b/Godeps/_workspace/src/github.com/gosimple/slug/README.md @@ -4,9 +4,10 @@ slug Package `slug` generate slug from unicode string, URL-friendly slugify with multiple languages support. -[![GoDoc](https://godoc.org/github.com/dalu/slug?status.png)](https://godoc.org/github.com/dalu/slug) +[![GoDoc](https://godoc.org/github.com/gosimple/slug?status.png)](https://godoc.org/github.com/gosimple/slug) +[![Build Status](https://drone.io/github.com/gosimple/slug/status.png)](https://drone.io/github.com/gosimple/slug/latest) -[Documentation online](http://godoc.org/github.com/dalu/slug) +[Documentation online](http://godoc.org/github.com/gosimple/slug) ## Example @@ -37,9 +38,12 @@ multiple languages support. fmt.Println(textSub) // Will print 'sand-is-hot' } +### Requests or bugs? + + ## Installation - go get -u github.com/dalu/slug + go get -u github.com/gosimple/slug ## License diff --git a/Godeps/_workspace/src/github.com/dalu/slug/default_substitution.go b/Godeps/_workspace/src/github.com/gosimple/slug/default_substitution.go similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/slug/default_substitution.go rename to Godeps/_workspace/src/github.com/gosimple/slug/default_substitution.go diff --git a/Godeps/_workspace/src/github.com/dalu/slug/doc.go b/Godeps/_workspace/src/github.com/gosimple/slug/doc.go similarity index 91% rename from Godeps/_workspace/src/github.com/dalu/slug/doc.go rename to Godeps/_workspace/src/github.com/gosimple/slug/doc.go index 39f57b30eb4..ffbe2c223f5 100644 --- a/Godeps/_workspace/src/github.com/dalu/slug/doc.go +++ b/Godeps/_workspace/src/github.com/gosimple/slug/doc.go @@ -12,7 +12,7 @@ Example: package main import( - "github.com/dalu/slug" + "github.com/gosimple/slug" "fmt" ) @@ -35,5 +35,9 @@ Example: textSub := slug.Make("water is hot") fmt.Println(textSub) // Will print 'sand-is-hot' } + +Requests or bugs? + +https://github.com/gosimple/slug/issues */ package slug diff --git a/Godeps/_workspace/src/github.com/dalu/slug/languages_substitution.go b/Godeps/_workspace/src/github.com/gosimple/slug/languages_substitution.go similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/slug/languages_substitution.go rename to Godeps/_workspace/src/github.com/gosimple/slug/languages_substitution.go diff --git a/Godeps/_workspace/src/github.com/dalu/slug/slug.go b/Godeps/_workspace/src/github.com/gosimple/slug/slug.go similarity index 98% rename from Godeps/_workspace/src/github.com/dalu/slug/slug.go rename to Godeps/_workspace/src/github.com/gosimple/slug/slug.go index 85d614f941e..b2c7d62514e 100644 --- a/Godeps/_workspace/src/github.com/dalu/slug/slug.go +++ b/Godeps/_workspace/src/github.com/gosimple/slug/slug.go @@ -6,9 +6,10 @@ package slug import ( - "github.com/dalu/unidecode" "regexp" "strings" + + "github.com/rainycape/unidecode" ) var ( diff --git a/Godeps/_workspace/src/github.com/dalu/slug/slug_test.go b/Godeps/_workspace/src/github.com/gosimple/slug/slug_test.go similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/slug/slug_test.go rename to Godeps/_workspace/src/github.com/gosimple/slug/slug_test.go diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/.gitignore b/Godeps/_workspace/src/github.com/rainycape/unidecode/.gitignore similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/unidecode/.gitignore rename to Godeps/_workspace/src/github.com/rainycape/unidecode/.gitignore diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/LICENSE b/Godeps/_workspace/src/github.com/rainycape/unidecode/LICENSE similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/unidecode/LICENSE rename to Godeps/_workspace/src/github.com/rainycape/unidecode/LICENSE diff --git a/Godeps/_workspace/src/github.com/rainycape/unidecode/README.md b/Godeps/_workspace/src/github.com/rainycape/unidecode/README.md new file mode 100644 index 00000000000..9a109bcfdb2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/rainycape/unidecode/README.md @@ -0,0 +1,6 @@ +unidecode +========= + +Unicode transliterator in Golang - Replaces non-ASCII characters with their ASCII approximations. + +[![GoDoc](https://godoc.org/github.com/rainycape/unidecode?status.svg)](https://godoc.org/github.com/rainycape/unidecode) diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/decode.go b/Godeps/_workspace/src/github.com/rainycape/unidecode/decode.go similarity index 92% rename from Godeps/_workspace/src/github.com/dalu/unidecode/decode.go rename to Godeps/_workspace/src/github.com/rainycape/unidecode/decode.go index 028533bf425..fe74bf3d880 100644 --- a/Godeps/_workspace/src/github.com/dalu/unidecode/decode.go +++ b/Godeps/_workspace/src/github.com/rainycape/unidecode/decode.go @@ -5,12 +5,9 @@ import ( "encoding/binary" "io" "strings" - "sync" ) var ( - decoded = false - mutex sync.Mutex transliterations [65536][]rune transCount = rune(len(transliterations)) getUint16 = binary.LittleEndian.Uint16 diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/make_table.go b/Godeps/_workspace/src/github.com/rainycape/unidecode/make_table.go similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/unidecode/make_table.go rename to Godeps/_workspace/src/github.com/rainycape/unidecode/make_table.go diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/table.go b/Godeps/_workspace/src/github.com/rainycape/unidecode/table.go similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/unidecode/table.go rename to Godeps/_workspace/src/github.com/rainycape/unidecode/table.go diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/table.txt b/Godeps/_workspace/src/github.com/rainycape/unidecode/table.txt similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/unidecode/table.txt rename to Godeps/_workspace/src/github.com/rainycape/unidecode/table.txt diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/unidecode.go b/Godeps/_workspace/src/github.com/rainycape/unidecode/unidecode.go similarity index 86% rename from Godeps/_workspace/src/github.com/dalu/unidecode/unidecode.go rename to Godeps/_workspace/src/github.com/rainycape/unidecode/unidecode.go index fa414bb0954..f9d2d49d418 100644 --- a/Godeps/_workspace/src/github.com/dalu/unidecode/unidecode.go +++ b/Godeps/_workspace/src/github.com/rainycape/unidecode/unidecode.go @@ -4,15 +4,15 @@ package unidecode import ( + "sync" "unicode" - - "gopkgs.com/pool.v1" ) const pooledCapacity = 64 var ( - slicePool = pool.New(0) + slicePool sync.Pool + decodingOnce sync.Once ) // Unidecode implements a unicode transliterator, which @@ -23,14 +23,7 @@ var ( // with their closest ASCII counterparts. // e.g. Unicode("áéíóú") => "aeiou" func Unidecode(s string) string { - if !decoded { - mutex.Lock() - if !decoded { - decodeTransliterations() - decoded = true - } - mutex.Unlock() - } + decodingOnce.Do(decodeTransliterations) l := len(s) var r []rune if l > pooledCapacity { diff --git a/Godeps/_workspace/src/github.com/dalu/unidecode/unidecode_test.go b/Godeps/_workspace/src/github.com/rainycape/unidecode/unidecode_test.go similarity index 100% rename from Godeps/_workspace/src/github.com/dalu/unidecode/unidecode_test.go rename to Godeps/_workspace/src/github.com/rainycape/unidecode/unidecode_test.go diff --git a/Godeps/_workspace/src/gopkgs.com/pool.v1/.gitignore b/Godeps/_workspace/src/gopkgs.com/pool.v1/.gitignore deleted file mode 100644 index 836562412fe..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/pool.v1/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# 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 -*.test diff --git a/Godeps/_workspace/src/gopkgs.com/pool.v1/LICENSE b/Godeps/_workspace/src/gopkgs.com/pool.v1/LICENSE deleted file mode 100644 index ad410e11302..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/pool.v1/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/Godeps/_workspace/src/gopkgs.com/pool.v1/README.md b/Godeps/_workspace/src/gopkgs.com/pool.v1/README.md deleted file mode 100644 index f9c561dc067..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/pool.v1/README.md +++ /dev/null @@ -1,13 +0,0 @@ -pool -==== - -sync.Pool compatibility layer for for Go - falls back to a channel based pool in Go < 1.3 - - -Please, use the following import path to ensure a stable API: - -```go - import "gopkgs.com/pool.v1" -``` - -View other available versions, documentation and examples at http://gopkgs.com/pool diff --git a/Godeps/_workspace/src/gopkgs.com/pool.v1/doc.go b/Godeps/_workspace/src/gopkgs.com/pool.v1/doc.go deleted file mode 100644 index 7546db7ac21..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/pool.v1/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package pool provides a sync.Pool compatibility layer, which -// falls back to a channel based pool on Go < 1.3. -package pool diff --git a/Godeps/_workspace/src/gopkgs.com/pool.v1/example_test.go b/Godeps/_workspace/src/gopkgs.com/pool.v1/example_test.go deleted file mode 100644 index dff9d348b3f..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/pool.v1/example_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package pool_test - -import ( - "fmt" - - "gopkgs.com/pool.v1" -) - -func ExamplePool() { - p := pool.New(0) - p.Put("Hello") - fmt.Println(p.Get()) - // OutPut: Hello -} - -func ExamplePoolNew() { - p := pool.New(0) - p.New = func() interface{} { - return "World!" - } - fmt.Println(p.Get()) - // OutPut: World! -} diff --git a/Godeps/_workspace/src/gopkgs.com/pool.v1/gopkgs.go b/Godeps/_workspace/src/gopkgs.com/pool.v1/gopkgs.go deleted file mode 100644 index 394a97806d6..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/pool.v1/gopkgs.go +++ /dev/null @@ -1,24 +0,0 @@ -package pool - -import ( - "fmt" - "reflect" -) - -// gopkgs.go: v1 - -// NOTE: This file is autogenerated by gopkgs.com. -const ( - goPkgsSrcPath = "github.com/rainycape/pool" - goPkgsName = "pool" - goPkgsErrFmt = "invalid import path %s - please use gopkgs.com/%s.v1 or see http://gopkgs.com/%s" -) - -type goPkgsCheck struct{} - -func init() { - typ := reflect.TypeOf(goPkgsCheck{}) - if typ.PkgPath() == goPkgsSrcPath { - panic(fmt.Errorf(goPkgsErrFmt, typ.PkgPath(), goPkgsName, goPkgsName)) - } -} diff --git a/Godeps/_workspace/src/gopkgs.com/pool.v1/pool.go b/Godeps/_workspace/src/gopkgs.com/pool.v1/pool.go deleted file mode 100644 index 269a2afd919..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/pool.v1/pool.go +++ /dev/null @@ -1,37 +0,0 @@ -// +build go1.3,!appengine - -package pool - -import ( - "sync" -) - -// Pool is a thin compatibility type to allow Go -// libraries to use the new sync.Pool in Go 1.3, -// while remaining compatible with lower Go versions. -// For more information, see the sync.Pool type. -type Pool sync.Pool - -// New returns a new Pool. The size argument is -// ignored on Go >= 1.3. In Go < 1.3, if size is -// zero, it's set to runtime.GOMAXPROCS(0) * 2. -func New(size int) *Pool { - return &Pool{} -} - -// Get returns an arbitrary previously Put value, removing -// it from the pool, or nil if there are no such values. Note -// that callers should not assume anything about the Get return -// value, since the runtime might decide to collect the elements -// from the pool at any time. -// -// If there are no elements to return and the New() field is non-nil, -// Get returns the result of calling it. -func (p *Pool) Get() interface{} { - return (*sync.Pool)(p).Get() -} - -// Put adds x to the pool. -func (p *Pool) Put(x interface{}) { - (*sync.Pool)(p).Put(x) -} diff --git a/Godeps/_workspace/src/gopkgs.com/pool.v1/pool_go1.2.go b/Godeps/_workspace/src/gopkgs.com/pool.v1/pool_go1.2.go deleted file mode 100644 index 11c4e490a30..00000000000 --- a/Godeps/_workspace/src/gopkgs.com/pool.v1/pool_go1.2.go +++ /dev/null @@ -1,57 +0,0 @@ -// +build !go1.3 appengine - -package pool - -import ( - "runtime" -) - -// Pool is a thin compatibility type to allow Go -// libraries to use the new sync.Pool in Go 1.3, -// while remaining compatible with lower Go versions. -// For more information, see the sync.Pool type. -type Pool struct { - ch chan interface{} - // New specifies a function to generate - // a new value, when Get would otherwise - // return nil. - New func() interface{} -} - -// New returns a new Pool. The size argument is -// ignored on Go >= 1.3. In Go < 1.3, if size is -// zero, it's set to runtime.GOMAXPROCS(0) * 2. -func New(size int) *Pool { - if size == 0 { - size = runtime.GOMAXPROCS(0) * 2 - } - return &Pool{ch: make(chan interface{}, size)} -} - -// Get returns an arbitrary previously Put value, removing -// it from the pool, or nil if there are no such values. Note -// that callers should not assume anything about the Get return -// value, since the runtime might decide to collect the elements -// from the pool at any time. -// -// If there are no elements to return and the New() field is non-nil, -// Get returns the result of calling it. -func (p *Pool) Get() interface{} { - select { - case x := <-p.ch: - return x - default: - } - if p.New != nil { - return p.New() - } - return nil -} - -// Put adds x to the pool. -func (p *Pool) Put(x interface{}) { - select { - case p.ch <- x: - default: - } -} diff --git a/bower.json b/bower.json new file mode 100644 index 00000000000..3607de4d8f0 --- /dev/null +++ b/bower.json @@ -0,0 +1,26 @@ +{ + "name": "grafana", + "version": "2.0.2", + "homepage": "https://github.com/grafana/grafana", + "authors": [], + "license": "Apache 2.0", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "public/vendor/", + "test", + "tests" + ], + "dependencies": { + "jquery": "~2.1.4", + "angular": "~1.4.0", + "angular-route": "~1.4.0", + "angular-mocks": "~1.4.0", + "angular-sanitize": "~1.4.0", + "angular-native-dragdrop": "~1.1.0", + "angular-bindonce": "~0.3.3", + "requirejs": "~2.1.18", + "requirejs-text": "~2.0.14" + } +} diff --git a/conf/defaults.ini b/conf/defaults.ini index e3e5f6eb5f9..a44d47e2daf 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -72,8 +72,9 @@ provider = file # Provider config options # memory: not have any config yet # file: session dir path, is relative to grafana data_path -# redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana` -# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` +# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` +# postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable +# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` provider_config = sessions @@ -167,6 +168,10 @@ token_url = https://accounts.google.com/o/oauth2/token api_url = https://www.googleapis.com/oauth2/v1/userinfo allowed_domains = +#################################### Basic Auth ########################## +[auth.basic] +enabled = true + #################################### Auth Proxy ########################## [auth.proxy] enabled = false @@ -177,7 +182,7 @@ auto_sign_up = true #################################### Auth LDAP ########################## [auth.ldap] enabled = true -hosts = ldap://localhost.com:389 +hosts = ldap://127.0.0.1:389 use_ssl = false base_dn = dc=grafana,dc=org bind_path = cn=%username%,dc=grafana,dc=org @@ -186,6 +191,21 @@ attr_name = cn attr_surname = sn attr_email = email +#################################### SMTP / Emailing ########################## +[smtp] +enabled = false +host = localhost:25 +user = +password = +cert_file = +key_file = +skip_verify = false +from_address = admin@grafana.localhost + +[emails] +welcome_email_on_sign_up = false +templates_pattern = emails/*.html + #################################### Logging ########################## [log] # Either "console", "file", default is "console" diff --git a/conf/sample.ini b/conf/sample.ini index 3c2773fa674..ef082ccff98 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -72,8 +72,9 @@ # Provider config options # memory: not have any config yet # file: session dir path, is relative to grafana data_path -# redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana` -# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` +# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` +# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` +# postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable ;provider_config = sessions # Session cookie name @@ -173,6 +174,24 @@ ;header_property = username ;auto_sign_up = true +#################################### Basic Auth ########################## +[auth.basic] +;enabled = true + +#################################### SMTP / Emailing ########################## +[smtp] +;enabled = false +;host = localhost:25 +;user = +;password = +;cert_file = +;key_file = +;skip_verify = false +;from_address = admin@grafana.localhost + +[emails] +;welcome_email_on_sign_up = false + #################################### Logging ########################## [log] # Either "console", "file", default is "console" diff --git a/docker/blocks/smtp/Dockerfile b/docker/blocks/smtp/Dockerfile new file mode 100644 index 00000000000..c1a3adba7c8 --- /dev/null +++ b/docker/blocks/smtp/Dockerfile @@ -0,0 +1,13 @@ +FROM centos:centos7 +MAINTAINER Przemyslaw Ozgo + +RUN \ + yum update -y && \ + yum install -y net-snmp net-snmp-utils && \ + yum clean all + +COPY bootstrap.sh /tmp/bootstrap.sh + +EXPOSE 161 + +ENTRYPOINT ["/tmp/bootstrap.sh"] diff --git a/docker/blocks/smtp/bootstrap.sh b/docker/blocks/smtp/bootstrap.sh new file mode 100755 index 00000000000..a78f9d6dc16 --- /dev/null +++ b/docker/blocks/smtp/bootstrap.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -u + +# User params +USER_PARAMS=$@ + +# Internal params +RUN_CMD="snmpd -f ${USER_PARAMS}" + +####################################### +# Echo/log function +# Arguments: +# String: value to log +####################################### +log() { + if [[ "$@" ]]; then echo "[`date +'%Y-%m-%d %T'`] $@"; + else echo; fi +} + +# Launch +log $RUN_CMD +$RUN_CMD + +# Exit immidiately in case of any errors or when we have interactive terminal +if [[ $? != 0 ]] || test -t 0; then exit $?; fi +log diff --git a/docker/blocks/smtp/fig b/docker/blocks/smtp/fig new file mode 100644 index 00000000000..c2d37e01c21 --- /dev/null +++ b/docker/blocks/smtp/fig @@ -0,0 +1,4 @@ +snmpd: + build: blocks/snmpd + ports: + - "161:161" diff --git a/docs/VERSION b/docs/VERSION index edb49bc725f..7ec1d6db408 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -2.0.0-beta +2.1.0 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 5024fd33afe..13eaf757c94 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -45,7 +45,7 @@ pages: - ['reference/graph.md', 'Reference', 'Graph Panel'] - ['reference/singlestat.md', 'Reference', 'Singlestat Panel'] -- ['reference/dashlist.md', 'Reference', 'Dashlist Panel'] +- ['reference/dashlist.md', 'Reference', 'Dashboard list Panel'] - ['reference/sharing.md', 'Reference', 'Sharing'] - ['reference/annotations.md', 'Reference', 'Annotations'] - ['reference/timerange.md', 'Reference', 'Time range controls'] @@ -60,6 +60,7 @@ pages: - ['datasources/graphite.md', 'Data Sources', 'Graphite'] - ['datasources/influxdb.md', 'Data Sources', 'InfluxDB'] - ['datasources/opentsdb.md', 'Data Sources', 'OpenTSDB'] +- ['datasources/kairosdb.md', 'Data Sources', 'KairosDB'] - ['project/building_from_source.md', 'Project', 'Building from source'] - ['project/cla.md', 'Project', 'Contributor License Agreement'] diff --git a/docs/sources/datasources/graphite.md b/docs/sources/datasources/graphite.md index acf4a78aaa1..d41a987514c 100644 --- a/docs/sources/datasources/graphite.md +++ b/docs/sources/datasources/graphite.md @@ -6,9 +6,9 @@ page_keywords: grafana, graphite, metrics, query, documentation # Graphite -Grafana has an advanced graphite query editor that lets you quickly navigate the metric space, add functions. -Change function paramaters and much more. The editor cannot handle all types of queries yet. -To switch to a regular text box click the pen icon to the right. +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. ## Adding the data source to Grafana Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you @@ -52,8 +52,21 @@ Some functions like aliasByNode support an optional second argument. To add this ## Point consolidation -All graphite metrics are consolidated so that graphite doesn't return more data points than there are pixels in the graph. By default -this consolidation is done using `avg` function. You can how graphite consolidates metrics by adding the Graphite consolidateBy function. +All Graphite metrics are consolidated so that Graphite doesn't return more data points than there are pixels in the graph. By default +this consolidation is done using `avg` function. You can how Graphite consolidates metrics by adding the Graphite consolidateBy function. > *Notice* This means that legend summary values (max, min, total) cannot be all correct at the same time. They are calculated > client side by Grafana. And depending on your consolidation function only one or two can be correct at the same time. + +## Templating +You can create a template variable in Grafana and have that variable filled with values from any Graphite metric exploration query. +You can then use this variable in your Graphite queries, either as part of a metric path or as arguments to functions. + +For example a query like `prod.servers.*` will fill the variable with all possible +values that exists in the wildcard position. + +You can also create nested variables that use other variables in their definition. For example +`apps.$app.servers.*` uses the variable `$app` in its query definition. + +![](/img/v2/templated_variable_parameter.png) + diff --git a/docs/sources/datasources/influxdb.md b/docs/sources/datasources/influxdb.md index 04fbd520a7e..05e627967eb 100644 --- a/docs/sources/datasources/influxdb.md +++ b/docs/sources/datasources/influxdb.md @@ -4,10 +4,11 @@ page_description: InfluxDB query guide page_keywords: grafana, influxdb, metrics, query, documentation --- - # InfluxDB -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. InfluxDB 0.9.x data source support is provided on an experimental basis. +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. ## Adding the data source to Grafana Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you @@ -31,37 +32,73 @@ Password | Database user's password > *Note* When using Proxy access mode the InfluxDB database, user and password will be hidden from the browser/frontend. When > using direct access mode all users will be able to see the database user & password. -## InfluxDB 0.9.x query editor +## InfluxDB 0.9.x -This editor & data source is not compatible with InfluxDB 0.8.x, please use the right data source for you InfluxDB version. -The InfluxDB 0.9.x editor is currently under development and is not yet fully usable. +![](/img/influxdb/InfluxDB_09_editor.png) -## InfluxDB 0.8.x query editor +You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the +panel title, then edit. The editor allows you to select metrics and tags. + +### Editor tag filters +To add a tag filter click the plus icon to the right of the `WHERE` condition. You can remove tag filters by clicking on +the tag key and select `--remove tag filter--`. + +### Regex matching +You can type in regex patterns for metric names or tag filter values, be sure to wrap the regex pattern in forward slashes (`/`). Grafana +will automaticallay adjust the filter tag condition to use the InfluxDB regex match condition operator (`=~`). + +### Editor group by +To group by a tag click the plus icon after the `GROUP BY ($interval)` text. Pick a tag from the dropdown that appears. +You can remove the group by by clicking on the tag and then select `--remove group by--` from the dropdown. + +### Editor RAW Query +You can switch to raw query mode by pressing the pen icon. + +> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter` clause and ends with `order by asc`. +> Also please always have a group by time and an aggregation function, otherwise InfluxDB can easily return hundreds of thousands +> of data points that will hang the browser. + +### Alias patterns + +- $m = replaced with measurement name +- $measurement = replaced with measurement name +- $tag_hostname = replaced with the value of the hostname tag +- You can also use [[tag_hostname]] pattern replacement syntax + +### Templating +You can create a template variable in Grafana and have that variable filled with values from any InfluxDB metric exploration query. +You can then use this variable in your InfluxDB metric queries. + +For example you can have a variable that contains all values for tag `hostname` if you specify a query like this +in the templating edit view. +```sql +SHOW TAG VALUES WITH KEY = "hostname" +``` + +You can also create nested variables. For example if you had another variable, for example `region`. Then you could have +the hosts variable only show hosts from the current selected region with a query like this: + +```sql +SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/ +``` + +> Always you `regex values` or `regex wildcard` for All format or multi select format. + +![](/img/influxdb/templating_simple_ex1.png) + +### Annotations +Annotations allows you to overlay rich event information on top of graphs. + +An example query: + +```SQL +SELECT title, description from events WHERE $timeFilter order asc +``` + +### InfluxDB 0.8.x ![](/img/v1/influxdb_editor.png) -When you add an InfluxDB query you can specify series name (can be regex), value column and a function. Group by time can be specified or if left blank will be automatically set depending on how long the current time span is. It will translate to a InfluxDB query that looks like this: - -```sql -select [[func]]([[column]]) from [[series]] where [[timeFilter]] group by time([[interval]]) order asc -``` - -To write the complete query yourself click the cog wheel icon to the right and select ``Raw query mode``. - -## InfluxDB 0.9 Filters & Templates queries - -The InfluxDB 0.9 data source does not currently support filters or templates. - -## InfluxDB 0.8 Filters & Templated queries - -![](/img/animated_gifs/influxdb_templated_query.gif) - - -Use a distinct influxdb query in the filter query input box: - -```sql -select distinct(host) from app.status -``` diff --git a/docs/sources/datasources/kairosdb.md b/docs/sources/datasources/kairosdb.md new file mode 100644 index 00000000000..f0d52b91548 --- /dev/null +++ b/docs/sources/datasources/kairosdb.md @@ -0,0 +1,47 @@ +--- +page_title: KairosDB Guide +page_description: KairosDB guide for Grafana +page_keywords: grafana, kairosdb, documentation +--- + +# KairosDB Guide + +## Adding the data source to Grafana +Open the side menu by clicking the the Grafana icon in the top header. In the side menu under the `Dashboards` link you +should find a link named `Data Sources`. If this link is missing in the side menu it means that your current +user does not have the `Admin` role for the current organization. + + + +Now click the `Add new` link in the top header. + +Name | Description +------------ | ------------- +Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards. +Default | Default data source means that it will be pre-selected for new panels. +Url | The http protocol, ip and port of your kairosdb server (default port is usually 8080) +Access | Proxy = access via Grafana backend, Direct = access directory from browser. + +## Query editor +Open a graph in edit mode by click the title. + + + +For details on KairosDB metric queries checkout the offical. + +- [Query Metrics - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/QueryMetrics.html). + +## Templated queries +KairosDB Datasource Plugin provides following functions in `Variables values query` field in Templating Editor to query `metric names`, `tag names`, and `tag values` to kairosdb server. + +Name | Description +---- | ---- +`metrics(query)` | Returns a list of metric names. If nothing is given, returns a list of all metric names. +`tag_names(query)` | Returns a list of tag names. If nothing is given, returns a list of all tag names. +`tag_values(query)` | Returns a list of tag values. If nothing is given, returns a list of all tag values. + +For details of `metric names`, `tag names`, and `tag values`, please refer to the KairosDB documentations. + +- [List Metric Names - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListMetricNames.html) +- [List Tag Names - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListTagNames.html) +- [List Tag Values - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/ListTagValues.html) diff --git a/docs/sources/datasources/opentsdb.md b/docs/sources/datasources/opentsdb.md index 4e85bdc9555..e9110418f4c 100644 --- a/docs/sources/datasources/opentsdb.md +++ b/docs/sources/datasources/opentsdb.md @@ -27,6 +27,18 @@ Open a graph in edit mode by click the title. ![](/img/v2/opentsdb_query_editor.png) +### Auto complete suggestions +You should get auto complete suggestions for tags and tag values. If you do not you need to enable `tsd.core.meta.enable_realtime_ts` in +the OpentSDB server settings. This is required for the OpenTSDB `lookup` api to work. + +## Templating queries + +When using OpenTSDB with a template variable of `query` type you can use following syntax for lookup. + + metrics() // returns metric names + tag_names(cpu) // return tag names (i.e. keys) for a specific cpu metric + tag_values(cpu, hostname) // return tag values for metric cpu and tag key hostname + For details on opentsdb metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html) diff --git a/docs/sources/guides/gettingstarted.md b/docs/sources/guides/gettingstarted.md index 641dc9516f5..4bb250ef55c 100644 --- a/docs/sources/guides/gettingstarted.md +++ b/docs/sources/guides/gettingstarted.md @@ -40,6 +40,14 @@ in the main Time Picker in the upper right, but they can also have relative time 5. Dashboard panel. You edit panels by clicking the panel title. 6. Graph legend. You can change series colors, y-axis and series visibility directly from the legend. +## Adding & Editing Graphs and Panels + +![](/img/v2/graph_metrics_tab_graphite.png) + +1. You add panels via row menu. The row menu is the green icon to the left of each row. +2. To edit the graph you click on the graph title to open the panel menu, then `Edit`. +3. This should take you to the `Metrics` tab. In this tab you should see the editor for your default data source. + ## Drag-and-Drop panels You can Drag-and-Drop Panels within and between Rows. Click and hold the Panel title, and drag it to its new location. diff --git a/docs/sources/index.md b/docs/sources/index.md index fe367c7f87a..491ff31377d 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -10,7 +10,7 @@ It provides a powerful and elegant way to create, share, and explore data and da Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control. -Grafana features pluggable panels and data sources allowing easy extensibility. There is currently rich support for [Graphite](http://graphite.readthedocs.org/en/latest/), [InfluxDB](http://influxdb.org) and [OpenTSDB](http://opentsdb.net). There is also experimental support for KairosDB, and SQL is on the roadmap. Grafana has a variety of panels, including a fully featured graph panel with rich visualization options. +Grafana features pluggable panels and data sources allowing easy extensibility. There is currently rich support for [Graphite](http://graphite.readthedocs.org/en/latest/), [InfluxDB](http://influxdb.org) and [OpenTSDB](http://opentsdb.net). There is also experimental support for [KairosDB](https://github.com/kairosdb/kairosdb), and SQL is on the roadmap. Grafana has a variety of panels, including a fully featured graph panel with rich visualization options. Version 2.0 was released in April 2015: Grafana now ships with its own backend server that brings [many changes and features](../guides/whats-new-in-v2/). diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 80ad5909a34..6310ccb7973 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -18,7 +18,7 @@ specified in a `.ini` configuration file or specified using environment variable > **Note.** If you have installed Grafana using the `deb` or `rpm` > packages, then your configuration file is located at > `/etc/grafana/grafana.ini`. This path is specified in the Grafana -> init.d script using `--config` file parameter. +> init.d script using `-config` file parameter. ## Using environment variables @@ -28,14 +28,19 @@ using environment variables using the syntax: GF__ Where the section name is the text within the brackets. Everything -should be upper case. For example, given this configuration setting: +should be upper case, `.` should be replaced by `_`. For example, given these configuration settings: [security] admin_user = admin + [auth.google] + client_secret = 0ldS3cretKey + + Then you can override that using: export GF_SECURITY_ADMIN_USER=true + export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey
@@ -322,7 +327,8 @@ This option should be configured differently depending on what type of session provider you have configured. - **file:** session file path, e.g. `data/sessions` -- **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name` +- **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` +- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=disable If you use MySQL or Postgres as the session store you need to create the session table manually. @@ -361,3 +367,14 @@ enabled. Counters are sent every 24 hours. Default value is `true`. If you want to track Grafana usage via Google analytics specify *your* Universal Analytics ID here. By default this feature is disabled. + +## [dashboards.json] + +If you have a system that automatically builds dashboards as json files you can enable this feature to have the +Grafana backend index those json dashboards which will make them appear in regular dashboard search. + +### enabled +`true` or `false`. Is disabled by default. + +### path +The full path to a directory containing your json dashboards. diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index bc0cba71732..c11de0faa7b 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -24,7 +24,7 @@ container: $ docker run -d -p 3000:3000 \ -v /var/lib/grafana:/var/lib/grafana \ - -e "GF_SECURITY_ADMIN_PASSWORD=secret \ + -e "GF_SECURITY_ADMIN_PASSWORD=secret" \ grafana/grafana:develop In the above example I map the data folder and sets a configuration option via diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 8401bf543de..dc08f5a261a 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -58,6 +58,8 @@ bra run Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin). ## Creating optimized release packages +This step builds linux packages and requires that fpm is installed. Install fpm via `gem install fpm`. + ``` go run build.go build package ``` diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md index 51c9ac1ba32..b0e84ef762b 100644 --- a/docs/sources/reference/annotations.md +++ b/docs/sources/reference/annotations.md @@ -13,7 +13,7 @@ you can get title, tags, and text information for the event. To add an annotation query click dashboard settings icon in top menu and select `Annotations` from the dropdown. This will open the `Annotations` edit view. Click the `Add` tab to add a new annotation query. -### Graphite annotations +## Graphite annotations Graphite supports two ways to query annotations. @@ -36,5 +36,4 @@ as the name for the fields that should be used for the annotation title, tags an For InfluxDB you need to enter a query like in the above screenshot. You need to have the ```where $timeFilter``` part. If you only select one column you will not need to enter anything in the column mapping fields. -If you have multiple columns you need to specify which column should be treated as title, tags and text column. diff --git a/docs/sources/reference/dashlist.md b/docs/sources/reference/dashlist.md index 6f591169f2c..9a54a18e288 100644 --- a/docs/sources/reference/dashlist.md +++ b/docs/sources/reference/dashlist.md @@ -6,4 +6,22 @@ page_keywords: grafana, dashlist, panel, documentation # Dashlist Panel +## Overview +![](/img/v2/dashboard_list_panel.png) + +The dashboard list panel allows you to show a list of links to other dashboards. The list +can be based on a search query or dashboard tag query. You can also configure it to show your starred +dashboards. + +## Options +![](/img/v2/dashboard_list_panel_options.png) + +Name | Description +------------ | ------------- +Mode | Set search or starred mode +Query | If in search mode specify the search query +Tags | if in search mode specify dashboard tags to search for +Limit number to | Specify the maximum number of dashboards + + diff --git a/docs/sources/reference/http_api.md b/docs/sources/reference/http_api.md index 9e24ccb39d7..90b7274cce6 100644 --- a/docs/sources/reference/http_api.md +++ b/docs/sources/reference/http_api.md @@ -183,7 +183,7 @@ Status Codes: ### Create data source -`PUT /api/datasources` +`POST /api/datasources` **Example Response**: @@ -192,9 +192,9 @@ Status Codes: {"message":"Datasource added"} -### Edit an existing data source +### Update an existing data source -`POST /api/datasources` +`PUT /api/datasources/:datasourceId` ### Delete an existing data source @@ -269,7 +269,7 @@ Adds a global user to the actual organisation. ### Delete User in Organisation -`DELETE /api/orgs/:orgId/users/:userId` +`DELETE /api/orgs/:orgId/users/:userId` ## Users diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index f66a8838441..397aaddec91 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -5,14 +5,43 @@ page_keywords: grafana, templating, variables, guide, documentation --- # Templated Dashboards +![](/img/v2/templating_var_list.png) -Templating feature can be enabled under dashboard settings, in the Features tab. The templating feature allows -you to create variables that can be used in your metric queries, series names and panel titles. Use this feature to -create generic dashboards that can quickly be changed to show graphs for different servers or metrics. +## Overview +Templating allows you to create dashboard variables that can be used in your metric queries, series +names and panel titles. Use this feature to create generic dashboards that can quickly be +changed to show graphs for different servers or metrics. + +You find this feature in the dashboard cog dropdown menu. + +## Variable types +There are three different types of template variables. They can all be used in the +same way but they differ in how the list variables values is created. + +### Query +This is the most common type of variable. It allows you to create a variable +with values fetched directly from a data source via a metric exploration query. + +For example a query like `prod.servers.*` will fill the variable with all possible +values that exists in the wildcard position (Graphite example). + +You can also create nested variables that use other variables in their definition. For example +`apps.$app.servers.*` uses the variable `$app` in its query definition. + +> For examples of template queries appropriate for your data source checkout the documentation +> page for your data source. + +### Interval +This variable type is useful for time ranges like `1m`,`1h`, `1d`. There is also an auto +option that will change depending on the current time range, you can specify how many times +the current time range should be divided to calculate the current `auto` range. + +![](/img/v2/templated_variable_parameter.png) + +### Custom +This variable type allow you to manually specify all the different values as a comma seperated +string. ## Screencast - Templated Graphite Queries -
-## Screencast - Templated InfluxDB Queries -Coming soon diff --git a/docs/sources/versions.html_fragment b/docs/sources/versions.html_fragment index 00ee000a0f0..55190af8b37 100644 --- a/docs/sources/versions.html_fragment +++ b/docs/sources/versions.html_fragment @@ -1,2 +1,3 @@ +
  • Version v2.1
  • Version v2.0
  • Version v1.9
  • diff --git a/main.go b/main.go index 5d5bddb7273..a732e1a166f 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,9 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/search" "github.com/grafana/grafana/pkg/services/eventpublisher" + "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" @@ -57,6 +58,10 @@ func main() { eventpublisher.Init() plugins.Init() + if err := notifications.Init(); err != nil { + log.Fatal(3, "Notification service failed to initialize", err) + } + if setting.ReportingEnabled { go metrics.StartUsageReportLoop() } diff --git a/package.json b/package.json index 71eb75b732b..4200da6951a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,6 @@ "grunt-jscs": "~1.5.x", "karma-sinon": "^1.0.3", "lodash": "^2.4.1", - "sinon": "^1.10.3" + "sinon": "1.10.3" } } diff --git a/packaging/deb/control/postinst b/packaging/deb/control/postinst index edb163ba7fb..7585d3f879d 100755 --- a/packaging/deb/control/postinst +++ b/packaging/deb/control/postinst @@ -43,9 +43,9 @@ case "$1" in chmod 755 /var/log/grafana /var/lib/grafana # configuration files should not be modifiable by grafana user, as this can be a security issue - chown -Rh root:root /etc/grafana/* + chown -Rh root:$GRAFANA_GROUP /etc/grafana/* chmod 755 /etc/grafana - find /etc/grafana -type f -exec chmod 644 {} ';' + find /etc/grafana -type f -exec chmod 640 {} ';' find /etc/grafana -type d -exec chmod 755 {} ';' # if $2 is set, this is an upgrade diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server index 6daebdb4331..a4f6423a68d 100755 --- a/packaging/deb/init.d/grafana-server +++ b/packaging/deb/init.d/grafana-server @@ -38,7 +38,12 @@ DAEMON=/usr/sbin/$NAME if [ `id -u` -ne 0 ]; then echo "You need root privileges to run this script" - exit 1 + exit 4 +fi + +if [ ! -x $DAEMON ]; then + echo "Program not installed or not executable" + exit 5 fi . /lib/lsb/init-functions @@ -54,9 +59,6 @@ fi DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}" -# Check DAEMON exists -test -x $DAEMON || exit 0 - case "$1" in start) @@ -137,8 +139,6 @@ case "$1" in ;; *) log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}" - exit 1 + exit 3 ;; esac - -exit 0 diff --git a/packaging/rpm/control/postinst b/packaging/rpm/control/postinst index 9e5e9accf79..fce80719115 100644 --- a/packaging/rpm/control/postinst +++ b/packaging/rpm/control/postinst @@ -43,9 +43,9 @@ if [ $1 -eq 1 ] ; then chmod 755 /var/log/grafana /var/lib/grafana # configuration files should not be modifiable by grafana user, as this can be a security issue - chown -Rh root:root /etc/grafana/* + chown -Rh root:$GRAFANA_GROUP /etc/grafana/* chmod 755 /etc/grafana - find /etc/grafana -type f -exec chmod 644 {} ';' + find /etc/grafana -type f -exec chmod 640 {} ';' find /etc/grafana -type d -exec chmod 755 {} ';' if [ -x /bin/systemctl ] ; then diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index 60f8d0ff46c..92e88673d74 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -35,6 +35,16 @@ MAX_OPEN_FILES=10000 PID_FILE=/var/run/$NAME.pid DAEMON=/usr/sbin/$NAME +if [ `id -u` -ne 0 ]; then + echo "You need root privileges to run this script" + exit 4 +fi + +if [ ! -x $DAEMON ]; then + echo "Program not installed or not executable" + exit 5 +fi + # # init.d / servicectl compatibility (openSUSE) # @@ -55,9 +65,6 @@ fi DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}" -# Check DAEMON exists -test -x $DAEMON || exit 0 - function isRunning() { status -p $PID_FILE $NAME > /dev/null 2>&1 } @@ -69,7 +76,7 @@ case "$1" in isRunning if [ $? -eq 0 ]; then echo "Already running." - exit 2 + exit 0 fi # Prepare environment @@ -90,7 +97,7 @@ case "$1" in # check if pid file has been written two if ! [[ -s $PID_FILE ]]; then echo "FAILED" - exit 3 + exit 1 fi i=0 timeout=10 @@ -101,7 +108,7 @@ case "$1" in i=$(($i + 1)) if [ $i -gt $timeout ]; then echo "FAILED" - exit 4 + exit 1 fi done fi @@ -131,6 +138,7 @@ case "$1" in ;; status) status -p $PID_FILE $NAME + exit $? ;; restart|force-reload) if [ -f "$PID_FILE" ]; then @@ -141,6 +149,6 @@ case "$1" in ;; *) echo -n "Usage: $0 {start|stop|restart|force-reload|status}" - exit 1 + exit 3 ;; esac diff --git a/pkg/api/api.go b/pkg/api/api.go index 0d8bceed4f8..fd4160f9993 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -26,6 +26,7 @@ func Register(r *macaron.Macaron) { // authed views r.Get("/profile/", reqSignedIn, Index) r.Get("/org/", reqSignedIn, Index) + r.Get("/org/new", reqSignedIn, Index) r.Get("/datasources/", reqSignedIn, Index) r.Get("/datasources/edit/*", reqSignedIn, Index) r.Get("/org/users/", reqSignedIn, Index) @@ -39,7 +40,14 @@ func Register(r *macaron.Macaron) { // sign up r.Get("/signup", Index) - r.Post("/api/user/signup", bind(m.CreateUserCommand{}), SignUp) + r.Post("/api/user/signup", bind(m.CreateUserCommand{}), wrap(SignUp)) + + // reset password + r.Get("/user/password/send-reset-email", Index) + r.Get("/user/password/reset", Index) + + r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), wrap(SendResetPasswordEmail)) + r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword)) // dashboard snapshots r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) diff --git a/pkg/api/common.go b/pkg/api/common.go index 4d8b3c28032..28e95866402 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -87,10 +87,10 @@ func ApiError(status int, message string, err error) *NormalResponse { switch status { case 404: - resp["message"] = "Not Found" - metrics.M_Api_Status_500.Inc(1) - case 500: metrics.M_Api_Status_404.Inc(1) + resp["message"] = "Not Found" + case 500: + metrics.M_Api_Status_500.Inc(1) resp["message"] = "Internal Server Error" } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index b010f32cdcb..a10c3c92f96 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -4,13 +4,14 @@ import ( "encoding/json" "os" "path" + "strings" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/search" + "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -31,7 +32,7 @@ func isDasboardStarredByUser(c *middleware.Context, dashId int64) (bool, error) func GetDashboard(c *middleware.Context) { metrics.M_Api_Dashboard_Get.Inc(1) - slug := c.Params(":slug") + slug := strings.ToLower(c.Params(":slug")) query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId} err := bus.Dispatch(&query) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 0061a2eba19..be044cc25eb 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -59,7 +59,7 @@ func GetDashboardSnapshot(c *middleware.Context) { // expired snapshots should also be removed from db if snapshot.Expires.Before(time.Now()) { - c.JsonApiErr(404, "Snapshot not found", err) + c.JsonApiErr(404, "Dashboard snapshot not found", err) return } diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index 0224ced599d..9b407535429 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -18,7 +18,7 @@ type AdminUpdateUserPasswordForm struct { } type AdminUpdateUserPermissionsForm struct { - IsGrafanaAdmin bool `json:"IsGrafanaAdmin" binding:"Required"` + IsGrafanaAdmin bool `json:"IsGrafanaAdmin"` } type AdminUserListItem struct { @@ -27,3 +27,13 @@ type AdminUserListItem struct { Login string `json:"login"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` } + +type SendResetPasswordEmailForm struct { + UserOrEmail string `json:"userOrEmail" binding:"Required"` +} + +type ResetUserPasswordForm struct { + Code string `json:"code"` + NewPassword string `json:"newPassword"` + ConfirmPassword string `json:"confirmPassword"` +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 4dd6ba06819..7851f1d8f0d 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -99,7 +99,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro "defaultDatasource": defaultDatasource, "datasources": datasources, "appSubUrl": setting.AppSubUrl, - "viewerRoleMode": setting.ViewerRoleMode, + "allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, "buildInfo": map[string]interface{}{ "version": setting.BuildVersion, "commit": setting.BuildCommit, diff --git a/pkg/api/ldapauth/ldapauth.go b/pkg/api/ldapauth/ldapauth.go index ea6c0421e12..c2973615544 100644 --- a/pkg/api/ldapauth/ldapauth.go +++ b/pkg/api/ldapauth/ldapauth.go @@ -5,7 +5,7 @@ import ( "fmt" "net/url" - "github.com/gogits/gogs/modules/ldap" + "github.com/go-ldap/ldap" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" ) @@ -15,7 +15,7 @@ var ( ) func Login(username, password string) error { - url, err := url.Parse(setting.LdapUrls[0]) + url, err := url.Parse(setting.LdapHosts[0]) if err != nil { return err } diff --git a/pkg/api/login.go b/pkg/api/login.go index f6a511bffbe..ab71e8d6445 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -4,7 +4,6 @@ import ( "net/url" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/api/ldapauth" "github.com/grafana/grafana/pkg/auth" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" @@ -89,28 +88,20 @@ func LoginApiPing(c *middleware.Context) { } func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response { - sourcesQuery := auth.GetAuthSourcesQuery{} - if err := bus.Dispatch(&sourcesQuery); err != nil { - return ApiError(500, "Could not get login sources", err) + authQuery := auth.AuthenticateUserQuery{ + Username: cmd.User, + Password: cmd.Password, } - var err error - var user *m.User - - for _, authSource := range sourcesQuery.Sources { - user, err = authSource.AuthenticateUser(cmd.User, cmd.Password) - if err == nil { - break - } - // handle non invalid credentials error, otherwise try next auth source - if err != auth.ErrInvalidCredentials { - return ApiError(500, "Error while trying to authenticate user", err) + if err := bus.Dispatch(&authQuery); err != nil { + if err == auth.ErrInvalidCredentials { + return ApiError(401, "Invalid username or password", err) } + + return ApiError(500, "Error while trying to authenticate user", err) } - if err != nil { - return ApiError(401, "Invalid username or password", err) - } + user := authQuery.User loginUserWithUser(user, c) @@ -128,19 +119,6 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response { return Json(200, result) } -func LoginUsingLdap(c *middleware.Context, cmd dtos.LoginCommand) Response { - err := ldapauth.Login(cmd.User, cmd.Password) - - if err != nil { - if err == ldapauth.ErrInvalidCredentials { - return ApiError(401, "Invalid username or password", err) - } - return ApiError(500, "Ldap login failed", err) - } - - return Empty(401) -} - func loginUserWithUser(user *m.User, c *middleware.Context) { if user == nil { log.Error(3, "User login with nil user") diff --git a/pkg/api/org.go b/pkg/api/org.go index 75499f33f61..746281c5138 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -6,6 +6,7 @@ import ( "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" ) // GET /api/org @@ -39,7 +40,7 @@ func getOrgHelper(orgId int64) Response { // POST /api/orgs func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response { - if !setting.AllowUserOrgCreate && !c.IsGrafanaAdmin { + if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) { return ApiError(401, "Access denied", nil) } @@ -50,7 +51,10 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response { metrics.M_Api_Org_Create.Inc(1) - return ApiSuccess("Organization created") + return Json(200, &util.DynMap{ + "orgId": cmd.Result.Id, + "message": "Organization created", + }) } // PUT /api/org diff --git a/pkg/api/password.go b/pkg/api/password.go new file mode 100644 index 00000000000..f3c2b0b7058 --- /dev/null +++ b/pkg/api/password.go @@ -0,0 +1,49 @@ +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/util" +) + +func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEmailForm) Response { + userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail} + + if err := bus.Dispatch(&userQuery); err != nil { + return ApiError(404, "User does not exist", err) + } + + emailCmd := m.SendResetPasswordEmailCommand{User: userQuery.Result} + if err := bus.Dispatch(&emailCmd); err != nil { + return ApiError(500, "Failed to send email", err) + } + + return ApiSuccess("Email sent") +} + +func ResetPassword(c *middleware.Context, form dtos.ResetUserPasswordForm) Response { + query := m.ValidateResetPasswordCodeQuery{Code: form.Code} + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrInvalidEmailCode { + return ApiError(400, "Invalid or expired reset password code", nil) + } + return ApiError(500, "Unknown error validating email code", err) + } + + if form.NewPassword != form.ConfirmPassword { + return ApiError(400, "Passwords do not match", nil) + } + + cmd := m.ChangeUserPasswordCommand{} + cmd.UserId = query.Result.Id + cmd.NewPassword = util.EncodePassword(form.NewPassword, query.Result.Salt) + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to change user password", err) + } + + return ApiSuccess("User password changed") +} diff --git a/pkg/api/search.go b/pkg/api/search.go index e451ac398d6..035e59fa3f4 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -3,7 +3,7 @@ package api import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/search" + "github.com/grafana/grafana/pkg/services/search" ) func Search(c *middleware.Context) { diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 63bb34c72ac..77305caba70 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -9,24 +10,29 @@ import ( ) // POST /api/user/signup -func SignUp(c *middleware.Context, cmd m.CreateUserCommand) { +func SignUp(c *middleware.Context, cmd m.CreateUserCommand) Response { if !setting.AllowUserSignUp { - c.JsonApiErr(401, "User signup is disabled", nil) - return + return ApiError(401, "User signup is disabled", nil) } cmd.Login = cmd.Email if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "failed to create user", err) - return + return ApiError(500, "failed to create user", err) } user := cmd.Result + bus.Publish(&events.UserSignedUp{ + Id: user.Id, + Name: user.Name, + Email: user.Email, + Login: user.Login, + }) + loginUserWithUser(&user, c) - c.JsonOK("User created and logged in") - metrics.M_Api_User_SignUp.Inc(1) + + return ApiSuccess("User created and logged in") } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index a236663ed44..f0dec5e9689 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -38,36 +39,46 @@ type AuthSource interface { AuthenticateUser(username, password string) (*m.User, error) } -type GetAuthSourcesQuery struct { - Sources []AuthSource +type AuthenticateUserQuery struct { + Username string + Password string + User *m.User } func init() { - bus.AddHandler("auth", GetAuthSources) + bus.AddHandler("auth", AuthenticateUser) } -func GetAuthSources(query *GetAuthSourcesQuery) error { - query.Sources = []AuthSource{&GrafanaDBAuthSource{}} - return nil +func AuthenticateUser(query *AuthenticateUserQuery) error { + err := loginUsingGrafanaDB(query) + if err == nil || err != ErrInvalidCredentials { + return err + } + + if setting.LdapEnabled { + err = loginUsingLdap(query) + } + + return err } -type GrafanaDBAuthSource struct { -} +func loginUsingGrafanaDB(query *AuthenticateUserQuery) error { + userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username} -func (s *GrafanaDBAuthSource) AuthenticateUser(username, password string) (*m.User, error) { - userQuery := m.GetUserByLoginQuery{LoginOrEmail: username} - err := bus.Dispatch(&userQuery) - - if err != nil { - return nil, ErrInvalidCredentials + if err := bus.Dispatch(&userQuery); err != nil { + if err == m.ErrUserNotFound { + return ErrInvalidCredentials + } + return err } user := userQuery.Result - passwordHashed := util.EncodePassword(password, user.Salt) + passwordHashed := util.EncodePassword(query.Password, user.Salt) if passwordHashed != user.Password { - return nil, ErrInvalidCredentials + return ErrInvalidCredentials } - return user, nil + query.User = user + return nil } diff --git a/pkg/auth/ldap.go b/pkg/auth/ldap.go new file mode 100644 index 00000000000..4a9b524cdbb --- /dev/null +++ b/pkg/auth/ldap.go @@ -0,0 +1,55 @@ +package auth + +import ( + "fmt" + "net/url" + + "github.com/go-ldap/ldap" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +func loginUsingLdap(query *AuthenticateUserQuery) error { + url, err := url.Parse(setting.LdapHosts[0]) + if err != nil { + return err + } + + log.Info("Host: %v", url.Host) + conn, err := ldap.Dial("tcp", url.Host) + if err != nil { + return err + } + + defer conn.Close() + + bindFormat := "cn=%s,dc=grafana,dc=org" + + nx := fmt.Sprintf(bindFormat, query.Username) + err = conn.Bind(nx, query.Password) + + if err != nil { + if ldapErr, ok := err.(*ldap.Error); ok { + if ldapErr.ResultCode == 49 { + return ErrInvalidCredentials + } + } + return err + } + + userQuery := m.GetUserByLoginQuery{LoginOrEmail: "admin"} + err = bus.Dispatch(&userQuery) + + if err != nil { + if err == m.ErrUserNotFound { + return ErrInvalidCredentials + } + return err + } + + query.User = userQuery.Result + + return nil +} diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index 80b80698a1c..6eb4b741a27 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -1,7 +1,7 @@ package bus import ( - "errors" + "fmt" "reflect" ) @@ -39,7 +39,7 @@ func (b *InProcBus) Dispatch(msg Msg) error { var handler = b.handlers[msgName] if handler == nil { - return errors.New("handler not found") + return fmt.Errorf("handler not found for %s", msgName) } var params = make([]reflect.Value, 1) diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index 6590c32fc93..c94661a5f9a 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -33,6 +33,7 @@ func newMacaron() *macaron.Macaron { mapStatic(m, "css", "css") mapStatic(m, "img", "img") mapStatic(m, "fonts", "fonts") + mapStatic(m, "robots.txt", "robots.txxt") m.Use(macaron.Renderer(macaron.RenderOptions{ Directory: path.Join(setting.StaticRootPath, "views"), diff --git a/pkg/components/ldap/LICENSE b/pkg/components/ldap/LICENSE deleted file mode 100644 index 74487567632..00000000000 --- a/pkg/components/ldap/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012 The Go 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. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -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 -OWNER 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/pkg/components/ldap/README b/pkg/components/ldap/README deleted file mode 100644 index edb54de0ac5..00000000000 --- a/pkg/components/ldap/README +++ /dev/null @@ -1,33 +0,0 @@ -Basic LDAP v3 functionality for the GO programming language. - -Required Librarys: - github.com/johnweldon/asn1-ber - -Working: - Connecting to LDAP server - Binding to LDAP server - Searching for entries - Compiling string filters to LDAP filters - Paging Search Results - Modify Requests / Responses - -Examples: - search - modify - -Tests Implemented: - Filter Compile / Decompile - -TODO: - Add Requests / Responses - Delete Requests / Responses - 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 diff --git a/pkg/components/ldap/_examples/enterprise.ldif b/pkg/components/ldap/_examples/enterprise.ldif deleted file mode 100644 index f0ec28f16be..00000000000 --- a/pkg/components/ldap/_examples/enterprise.ldif +++ /dev/null @@ -1,63 +0,0 @@ -dn: dc=enterprise,dc=org -objectClass: dcObject -objectClass: organization -o: acme - -dn: cn=admin,dc=enterprise,dc=org -objectClass: person -cn: admin -sn: admin -description: "LDAP Admin" - -dn: ou=crew,dc=enterprise,dc=org -ou: crew -objectClass: organizationalUnit - - -dn: cn=kirkj,ou=crew,dc=enterprise,dc=org -cn: kirkj -sn: Kirk -gn: James Tiberius -mail: james.kirk@enterprise.org -objectClass: inetOrgPerson - -dn: cn=spock,ou=crew,dc=enterprise,dc=org -cn: spock -sn: Spock -mail: spock@enterprise.org -objectClass: inetOrgPerson - -dn: cn=mccoyl,ou=crew,dc=enterprise,dc=org -cn: mccoyl -sn: McCoy -gn: Leonard -mail: leonard.mccoy@enterprise.org -objectClass: inetOrgPerson - -dn: cn=scottm,ou=crew,dc=enterprise,dc=org -cn: scottm -sn: Scott -gn: Montgomery -mail: Montgomery.scott@enterprise.org -objectClass: inetOrgPerson - -dn: cn=uhuran,ou=crew,dc=enterprise,dc=org -cn: uhuran -sn: Uhura -gn: Nyota -mail: nyota.uhura@enterprise.org -objectClass: inetOrgPerson - -dn: cn=suluh,ou=crew,dc=enterprise,dc=org -cn: suluh -sn: Sulu -gn: Hikaru -mail: hikaru.sulu@enterprise.org -objectClass: inetOrgPerson - -dn: cn=chekovp,ou=crew,dc=enterprise,dc=org -cn: chekovp -sn: Chekov -gn: pavel -mail: pavel.chekov@enterprise.org -objectClass: inetOrgPerson diff --git a/pkg/components/ldap/_examples/modify.go b/pkg/components/ldap/_examples/modify.go deleted file mode 100644 index cd6dfc9eb71..00000000000 --- a/pkg/components/ldap/_examples/modify.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2014 The Go 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 main - -import ( - "errors" - "fmt" - "log" - - "github.com/gogits/gogs/modules/ldap" -) - -var ( - LdapServer string = "localhost" - LdapPort uint16 = 389 - BaseDN string = "dc=enterprise,dc=org" - BindDN string = "cn=admin,dc=enterprise,dc=org" - BindPW string = "enterprise" - Filter string = "(cn=kirkj)" -) - -func search(l *ldap.Conn, filter string, attributes []string) (*ldap.Entry, *ldap.Error) { - search := ldap.NewSearchRequest( - BaseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - filter, - attributes, - nil) - - sr, err := l.Search(search) - if err != nil { - log.Fatalf("ERROR: %s\n", err) - return nil, err - } - - log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) - if len(sr.Entries) == 0 { - return nil, ldap.NewError(ldap.ErrorDebugging, errors.New(fmt.Sprintf("no entries found for: %s", filter))) - } - return sr.Entries[0], nil -} - -func main() { - l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort)) - if err != nil { - log.Fatalf("ERROR: %s\n", err.Error()) - } - defer l.Close() - // l.Debug = true - - l.Bind(BindDN, BindPW) - - log.Printf("The Search for Kirk ... %s\n", Filter) - entry, err := search(l, Filter, []string{}) - if err != nil { - log.Fatal("could not get entry") - } - entry.PrettyPrint(0) - - log.Printf("modify the mail address and add a description ... \n") - modify := ldap.NewModifyRequest(entry.DN) - modify.Add("description", []string{"Captain of the USS Enterprise"}) - modify.Replace("mail", []string{"captain@enterprise.org"}) - if err := l.Modify(modify); err != nil { - log.Fatalf("ERROR: %s\n", err.Error()) - } - - entry, err = search(l, Filter, []string{}) - if err != nil { - log.Fatal("could not get entry") - } - entry.PrettyPrint(0) - - log.Printf("reset the entry ... \n") - modify = ldap.NewModifyRequest(entry.DN) - modify.Delete("description", []string{}) - modify.Replace("mail", []string{"james.kirk@enterprise.org"}) - if err := l.Modify(modify); err != nil { - log.Fatalf("ERROR: %s\n", err.Error()) - } - - entry, err = search(l, Filter, []string{}) - if err != nil { - log.Fatal("could not get entry") - } - entry.PrettyPrint(0) -} diff --git a/pkg/components/ldap/_examples/search.go b/pkg/components/ldap/_examples/search.go deleted file mode 100644 index 609256f4d3c..00000000000 --- a/pkg/components/ldap/_examples/search.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2014 The Go 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 main - -import ( - "fmt" - "log" - - "github.com/gogits/gogs/modules/ldap" -) - -var ( - ldapServer string = "adserver" - ldapPort uint16 = 3268 - baseDN string = "dc=*,dc=*" - filter string = "(&(objectClass=user)(sAMAccountName=*)(memberOf=CN=*,OU=*,DC=*,DC=*))" - Attributes []string = []string{"memberof"} - user string = "*" - passwd string = "*" -) - -func main() { - l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) - if err != nil { - log.Fatalf("ERROR: %s\n", err.Error()) - } - defer l.Close() - // l.Debug = true - - err = l.Bind(user, passwd) - if err != nil { - log.Printf("ERROR: Cannot bind: %s\n", err.Error()) - return - } - search := ldap.NewSearchRequest( - baseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - filter, - Attributes, - nil) - - sr, err := l.Search(search) - if err != nil { - log.Fatalf("ERROR: %s\n", err.Error()) - return - } - - log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) - sr.PrettyPrint(0) -} diff --git a/pkg/components/ldap/_examples/searchSSL.go b/pkg/components/ldap/_examples/searchSSL.go deleted file mode 100644 index aa9cbcc1249..00000000000 --- a/pkg/components/ldap/_examples/searchSSL.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2014 The Go 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 main - -import ( - "fmt" - "log" - - "github.com/gogits/gogs/modules/ldap" -) - -var ( - LdapServer string = "localhost" - LdapPort uint16 = 636 - BaseDN string = "dc=enterprise,dc=org" - Filter string = "(cn=kirkj)" - Attributes []string = []string{"mail"} -) - -func main() { - l, err := ldap.DialSSL("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort), nil) - if err != nil { - log.Fatalf("ERROR: %s\n", err.String()) - } - defer l.Close() - // l.Debug = true - - search := ldap.NewSearchRequest( - BaseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - Filter, - Attributes, - nil) - - sr, err := l.Search(search) - if err != nil { - log.Fatalf("ERROR: %s\n", err.String()) - return - } - - log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) - sr.PrettyPrint(0) -} diff --git a/pkg/components/ldap/_examples/searchTLS.go b/pkg/components/ldap/_examples/searchTLS.go deleted file mode 100644 index c771a8eda87..00000000000 --- a/pkg/components/ldap/_examples/searchTLS.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2014 The Go 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 main - -import ( - "fmt" - "log" - - "github.com/gogits/gogs/modules/ldap" -) - -var ( - LdapServer string = "localhost" - LdapPort uint16 = 389 - BaseDN string = "dc=enterprise,dc=org" - Filter string = "(cn=kirkj)" - Attributes []string = []string{"mail"} -) - -func main() { - l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort), nil) - if err != nil { - log.Fatalf("ERROR: %s\n", err.Error()) - } - defer l.Close() - // l.Debug = true - - search := ldap.NewSearchRequest( - BaseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - Filter, - Attributes, - nil) - - sr, err := l.Search(search) - if err != nil { - log.Fatalf("ERROR: %s\n", err.Error()) - return - } - - log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) - sr.PrettyPrint(0) -} diff --git a/pkg/components/ldap/_examples/slapd.conf b/pkg/components/ldap/_examples/slapd.conf deleted file mode 100644 index 5a66be0152d..00000000000 --- a/pkg/components/ldap/_examples/slapd.conf +++ /dev/null @@ -1,67 +0,0 @@ -# -# See slapd.conf(5) for details on configuration options. -# This file should NOT be world readable. -# -include /private/etc/openldap/schema/core.schema -include /private/etc/openldap/schema/cosine.schema -include /private/etc/openldap/schema/inetorgperson.schema - -# Define global ACLs to disable default read access. - -# Do not enable referrals until AFTER you have a working directory -# service AND an understanding of referrals. -#referral ldap://root.openldap.org - -pidfile /private/var/db/openldap/run/slapd.pid -argsfile /private/var/db/openldap/run/slapd.args - -# Load dynamic backend modules: -# modulepath /usr/libexec/openldap -# moduleload back_bdb.la -# moduleload back_hdb.la -# moduleload back_ldap.la - -# Sample security restrictions -# Require integrity protection (prevent hijacking) -# Require 112-bit (3DES or better) encryption for updates -# Require 63-bit encryption for simple bind -# security ssf=1 update_ssf=112 simple_bind=64 - -# Sample access control policy: -# Root DSE: allow anyone to read it -# Subschema (sub)entry DSE: allow anyone to read it -# Other DSEs: -# Allow self write access -# Allow authenticated users read access -# Allow anonymous users to authenticate -# Directives needed to implement policy: -# access to dn.base="" by * read -# access to dn.base="cn=Subschema" by * read -# access to * -# by self write -# by users read -# by anonymous auth -# -# if no access controls are present, the default policy -# allows anyone and everyone to read anything but restricts -# updates to rootdn. (e.g., "access to * by * read") -# -# rootdn can always read and write EVERYTHING! - -####################################################################### -# BDB database definitions -####################################################################### - -database bdb -suffix "dc=enterprise,dc=org" -rootdn "cn=admin,dc=enterprise,dc=org" -# Cleartext passwords, especially for the rootdn, should -# be avoid. See slappasswd(8) and slapd.conf(5) for details. -# Use of strong authentication encouraged. -rootpw {SSHA}laO00HsgszhK1O0Z5qR0/i/US69Osfeu -# The database directory MUST exist prior to running slapd AND -# should only be accessible by the slapd and slap tools. -# Mode 700 recommended. -directory /private/var/db/openldap/openldap-data -# Indices to maintain -index objectClass eq diff --git a/pkg/components/ldap/bind.go b/pkg/components/ldap/bind.go deleted file mode 100644 index 0561e611d1d..00000000000 --- a/pkg/components/ldap/bind.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2011 The Go 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 ldap - -import ( - "errors" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -func (l *Conn) Bind(username, password string) 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")) - bindRequest := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - bindRequest.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - bindRequest.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, username, "User Name")) - bindRequest.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, password, "Password")) - packet.AppendChild(bindRequest) - - if l.Debug { - ber.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) - - packet = <-channel - if packet == nil { - return NewError(ErrorNetwork, errors.New("ldap: could not retrieve response")) - } - - if l.Debug { - if err := addLDAPDescriptions(packet); err != nil { - return err - } - ber.PrintPacket(packet) - } - - resultCode, resultDescription := getLDAPResultCode(packet) - if resultCode != 0 { - return NewError(resultCode, errors.New(resultDescription)) - } - - return nil -} diff --git a/pkg/components/ldap/conn.go b/pkg/components/ldap/conn.go deleted file mode 100644 index 6a244f1253b..00000000000 --- a/pkg/components/ldap/conn.go +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright 2011 The Go 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 ldap - -import ( - "crypto/tls" - "errors" - "log" - "net" - "sync" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -const ( - MessageQuit = 0 - MessageRequest = 1 - MessageResponse = 2 - MessageFinish = 3 -) - -type messagePacket struct { - Op int - MessageID uint64 - Packet *ber.Packet - Channel chan *ber.Packet -} - -// Conn represents an LDAP Connection -type Conn struct { - conn net.Conn - isTLS bool - isClosing bool - Debug debugging - chanConfirm chan bool - chanResults map[uint64]chan *ber.Packet - chanMessage chan *messagePacket - chanMessageID chan uint64 - wgSender sync.WaitGroup - wgClose sync.WaitGroup - once sync.Once -} - -// Dial connects to the given address on the given network using net.Dial -// and then returns a new Conn for the connection. -func Dial(network, addr string) (*Conn, error) { - c, err := net.Dial(network, addr) - if err != nil { - return nil, NewError(ErrorNetwork, err) - } - conn := NewConn(c) - conn.start() - return conn, nil -} - -// DialTLS connects to the given address on the given network using tls.Dial -// and then returns a new Conn for the connection. -func DialTLS(network, addr string, config *tls.Config) (*Conn, error) { - c, err := tls.Dial(network, addr, config) - if err != nil { - return nil, NewError(ErrorNetwork, err) - } - conn := NewConn(c) - conn.isTLS = true - conn.start() - return conn, nil -} - -// NewConn returns a new Conn using conn for network I/O. -func NewConn(conn net.Conn) *Conn { - return &Conn{ - conn: conn, - chanConfirm: make(chan bool), - chanMessageID: make(chan uint64), - chanMessage: make(chan *messagePacket, 10), - chanResults: map[uint64]chan *ber.Packet{}, - } -} - -func (l *Conn) start() { - go l.reader() - go l.processMessages() - l.wgClose.Add(1) -} - -// Close closes the connection. -func (l *Conn) Close() { - l.once.Do(func() { - l.isClosing = true - l.wgSender.Wait() - - l.Debug.Printf("Sending quit message and waiting for confirmation") - l.chanMessage <- &messagePacket{Op: MessageQuit} - <-l.chanConfirm - close(l.chanMessage) - - l.Debug.Printf("Closing network connection") - if err := l.conn.Close(); err != nil { - log.Print(err) - } - - l.conn = nil - l.wgClose.Done() - }) - l.wgClose.Wait() -} - -// Returns the next available messageID -func (l *Conn) nextMessageID() uint64 { - if l.chanMessageID != nil { - if messageID, ok := <-l.chanMessageID; ok { - return messageID - } - } - return 0 -} - -// StartTLS sends the command to start a TLS session and then creates a new TLS Client -func (l *Conn) StartTLS(config *tls.Config) error { - messageID := l.nextMessageID() - - if l.isTLS { - return NewError(ErrorNetwork, errors.New("ldap: already encrypted")) - } - - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS") - request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command")) - packet.AppendChild(request) - l.Debug.PrintPacket(packet) - - _, err := l.conn.Write(packet.Bytes()) - if err != nil { - return NewError(ErrorNetwork, err) - } - - packet, err = ber.ReadPacket(l.conn) - if err != nil { - return NewError(ErrorNetwork, err) - } - - if l.Debug { - if err := addLDAPDescriptions(packet); err != nil { - return err - } - ber.PrintPacket(packet) - } - - if packet.Children[1].Children[0].Value.(uint64) == 0 { - conn := tls.Client(l.conn, config) - l.isTLS = true - l.conn = conn - } - - return nil -} - -func (l *Conn) sendMessage(packet *ber.Packet) (chan *ber.Packet, error) { - if l.isClosing { - return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) - } - out := make(chan *ber.Packet) - message := &messagePacket{ - Op: MessageRequest, - MessageID: packet.Children[0].Value.(uint64), - Packet: packet, - Channel: out, - } - l.sendProcessMessage(message) - return out, nil -} - -func (l *Conn) finishMessage(messageID uint64) { - if l.isClosing { - return - } - message := &messagePacket{ - Op: MessageFinish, - MessageID: messageID, - } - l.sendProcessMessage(message) -} - -func (l *Conn) sendProcessMessage(message *messagePacket) bool { - if l.isClosing { - return false - } - l.wgSender.Add(1) - l.chanMessage <- message - l.wgSender.Done() - return true -} - -func (l *Conn) processMessages() { - defer func() { - for messageID, channel := range l.chanResults { - l.Debug.Printf("Closing channel for MessageID %d", messageID) - close(channel) - delete(l.chanResults, messageID) - } - close(l.chanMessageID) - l.chanConfirm <- true - close(l.chanConfirm) - }() - - var messageID uint64 = 1 - for { - select { - case l.chanMessageID <- messageID: - messageID++ - case messagePacket, ok := <-l.chanMessage: - if !ok { - l.Debug.Printf("Shutting down - message channel is closed") - return - } - switch messagePacket.Op { - case MessageQuit: - l.Debug.Printf("Shutting down - quit message received") - return - case MessageRequest: - // Add to message list and write to network - l.Debug.Printf("Sending message %d", messagePacket.MessageID) - l.chanResults[messagePacket.MessageID] = messagePacket.Channel - // go routine - buf := messagePacket.Packet.Bytes() - - _, err := l.conn.Write(buf) - if err != nil { - l.Debug.Printf("Error Sending Message: %s", err.Error()) - break - } - case MessageResponse: - l.Debug.Printf("Receiving message %d", messagePacket.MessageID) - if chanResult, ok := l.chanResults[messagePacket.MessageID]; ok { - chanResult <- messagePacket.Packet - } else { - log.Printf("Received unexpected message %d", messagePacket.MessageID) - ber.PrintPacket(messagePacket.Packet) - } - case MessageFinish: - // Remove from message list - l.Debug.Printf("Finished message %d", messagePacket.MessageID) - close(l.chanResults[messagePacket.MessageID]) - delete(l.chanResults, messagePacket.MessageID) - } - } - } -} - -func (l *Conn) reader() { - defer func() { - l.Close() - }() - - for { - packet, err := ber.ReadPacket(l.conn) - if err != nil { - l.Debug.Printf("reader: %s", err.Error()) - return - } - addLDAPDescriptions(packet) - message := &messagePacket{ - Op: MessageResponse, - MessageID: packet.Children[0].Value.(uint64), - Packet: packet, - } - if !l.sendProcessMessage(message) { - return - } - - } -} diff --git a/pkg/components/ldap/control.go b/pkg/components/ldap/control.go deleted file mode 100644 index 4b15f1bd4a8..00000000000 --- a/pkg/components/ldap/control.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2011 The Go 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 ldap - -import ( - "fmt" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -const ( - ControlTypePaging = "1.2.840.113556.1.4.319" -) - -var ControlTypeMap = map[string]string{ - ControlTypePaging: "Paging", -} - -type Control interface { - GetControlType() string - Encode() *ber.Packet - String() string -} - -type ControlString struct { - ControlType string - Criticality bool - ControlValue string -} - -func (c *ControlString) GetControlType() string { - return c.ControlType -} - -func (c *ControlString) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlType, "Control Type ("+ControlTypeMap[c.ControlType]+")")) - if c.Criticality { - packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) - } - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlValue, "Control Value")) - return packet -} - -func (c *ControlString) String() string { - return fmt.Sprintf("Control Type: %s (%q) Criticality: %t Control Value: %s", ControlTypeMap[c.ControlType], c.ControlType, c.Criticality, c.ControlValue) -} - -type ControlPaging struct { - PagingSize uint32 - Cookie []byte -} - -func (c *ControlPaging) GetControlType() string { - return ControlTypePaging -} - -func (c *ControlPaging) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypePaging, "Control Type ("+ControlTypeMap[ControlTypePaging]+")")) - - p2 := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Paging)") - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Search Control Value") - seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(c.PagingSize), "Paging Size")) - cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") - cookie.Value = c.Cookie - cookie.Data.Write(c.Cookie) - seq.AppendChild(cookie) - p2.AppendChild(seq) - - packet.AppendChild(p2) - return packet -} - -func (c *ControlPaging) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t PagingSize: %d Cookie: %q", - ControlTypeMap[ControlTypePaging], - ControlTypePaging, - false, - c.PagingSize, - c.Cookie) -} - -func (c *ControlPaging) SetCookie(cookie []byte) { - c.Cookie = cookie -} - -func FindControl(controls []Control, controlType string) Control { - for _, c := range controls { - if c.GetControlType() == controlType { - return c - } - } - return nil -} - -func DecodeControl(packet *ber.Packet) Control { - ControlType := packet.Children[0].Value.(string) - Criticality := false - - packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" - value := packet.Children[1] - if len(packet.Children) == 3 { - value = packet.Children[2] - packet.Children[1].Description = "Criticality" - Criticality = packet.Children[1].Value.(bool) - } - - value.Description = "Control Value" - switch ControlType { - case ControlTypePaging: - value.Description += " (Paging)" - c := new(ControlPaging) - if value.Value != nil { - valueChildren := ber.DecodePacket(value.Data.Bytes()) - value.Data.Truncate(0) - value.Value = nil - value.AppendChild(valueChildren) - } - value = value.Children[0] - value.Description = "Search Control Value" - value.Children[0].Description = "Paging Size" - value.Children[1].Description = "Cookie" - c.PagingSize = uint32(value.Children[0].Value.(uint64)) - c.Cookie = value.Children[1].Data.Bytes() - value.Children[1].Value = c.Cookie - return c - } - c := new(ControlString) - c.ControlType = ControlType - c.Criticality = Criticality - c.ControlValue = value.Value.(string) - return c -} - -func NewControlString(controlType string, criticality bool, controlValue string) *ControlString { - return &ControlString{ - ControlType: controlType, - Criticality: criticality, - ControlValue: controlValue, - } -} - -func NewControlPaging(pagingSize uint32) *ControlPaging { - return &ControlPaging{PagingSize: pagingSize} -} - -func encodeControls(controls []Control) *ber.Packet { - packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") - for _, control := range controls { - packet.AppendChild(control.Encode()) - } - return packet -} diff --git a/pkg/components/ldap/debug.go b/pkg/components/ldap/debug.go deleted file mode 100644 index 67856fe7a60..00000000000 --- a/pkg/components/ldap/debug.go +++ /dev/null @@ -1,24 +0,0 @@ -package ldap - -import ( - "log" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -// debugging type -// - has a Printf method to write the debug output -type debugging bool - -// write debug output -func (debug debugging) Printf(format string, args ...interface{}) { - if debug { - log.Printf(format, args...) - } -} - -func (debug debugging) PrintPacket(packet *ber.Packet) { - if debug { - ber.PrintPacket(packet) - } -} diff --git a/pkg/components/ldap/filter.go b/pkg/components/ldap/filter.go deleted file mode 100644 index 0ad7a403bca..00000000000 --- a/pkg/components/ldap/filter.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2011 The Go 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 ldap - -import ( - "errors" - "fmt" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -const ( - FilterAnd = 0 - FilterOr = 1 - FilterNot = 2 - FilterEqualityMatch = 3 - FilterSubstrings = 4 - FilterGreaterOrEqual = 5 - FilterLessOrEqual = 6 - FilterPresent = 7 - FilterApproxMatch = 8 - FilterExtensibleMatch = 9 -) - -var FilterMap = map[uint64]string{ - FilterAnd: "And", - FilterOr: "Or", - FilterNot: "Not", - FilterEqualityMatch: "Equality Match", - FilterSubstrings: "Substrings", - FilterGreaterOrEqual: "Greater Or Equal", - FilterLessOrEqual: "Less Or Equal", - FilterPresent: "Present", - FilterApproxMatch: "Approx Match", - FilterExtensibleMatch: "Extensible Match", -} - -const ( - FilterSubstringsInitial = 0 - FilterSubstringsAny = 1 - FilterSubstringsFinal = 2 -) - -var FilterSubstringsMap = map[uint64]string{ - FilterSubstringsInitial: "Substrings Initial", - FilterSubstringsAny: "Substrings Any", - FilterSubstringsFinal: "Substrings Final", -} - -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 '('")) - } - packet, pos, err := compileFilter(filter, 1) - if err != nil { - return nil, err - } - if pos != len(filter) { - return nil, NewError(ErrorFilterCompile, errors.New("ldap: finished compiling filter with extra at end: "+fmt.Sprint(filter[pos:]))) - } - return packet, nil -} - -func DecompileFilter(packet *ber.Packet) (ret string, err error) { - defer func() { - if r := recover(); r != nil { - err = NewError(ErrorFilterDecompile, errors.New("ldap: error decompiling filter")) - } - }() - ret = "(" - err = nil - childStr := "" - - switch packet.Tag { - case FilterAnd: - ret += "&" - for _, child := range packet.Children { - childStr, err = DecompileFilter(child) - if err != nil { - return - } - ret += childStr - } - case FilterOr: - ret += "|" - for _, child := range packet.Children { - childStr, err = DecompileFilter(child) - if err != nil { - return - } - ret += childStr - } - case FilterNot: - ret += "!" - childStr, err = DecompileFilter(packet.Children[0]) - if err != nil { - return - } - ret += childStr - - case FilterSubstrings: - ret += ber.DecodeString(packet.Children[0].Data.Bytes()) - ret += "=" - switch packet.Children[1].Children[0].Tag { - case FilterSubstringsInitial: - ret += ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) + "*" - case FilterSubstringsAny: - ret += "*" + ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) + "*" - case FilterSubstringsFinal: - ret += "*" + ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) - } - case FilterEqualityMatch: - ret += ber.DecodeString(packet.Children[0].Data.Bytes()) - ret += "=" - ret += 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()) - case FilterLessOrEqual: - ret += ber.DecodeString(packet.Children[0].Data.Bytes()) - ret += "<=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) - case FilterPresent: - ret += ber.DecodeString(packet.Children[0].Data.Bytes()) - ret += "=*" - case FilterApproxMatch: - ret += ber.DecodeString(packet.Children[0].Data.Bytes()) - ret += "~=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) - } - - ret += ")" - return -} - -func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { - for pos < len(filter) && filter[pos] == '(' { - child, newPos, err := compileFilter(filter, pos+1) - if err != nil { - return pos, err - } - pos = newPos - parent.AppendChild(child) - } - if pos == len(filter) { - return pos, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) - } - - return pos + 1, nil -} - -func compileFilter(filter string, pos int) (*ber.Packet, int, error) { - var packet *ber.Packet - var err error - - defer func() { - if r := recover(); r != nil { - err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter")) - } - }() - - newPos := pos - switch filter[pos] { - case '(': - packet, newPos, err = compileFilter(filter, pos+1) - newPos++ - return packet, newPos, err - case '&': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd]) - newPos, err = compileFilterSet(filter, pos+1, packet) - return packet, newPos, err - case '|': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr]) - newPos, err = compileFilterSet(filter, pos+1, 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) - packet.AppendChild(child) - return packet, newPos, err - default: - attribute := "" - 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]) - } - newPos++ - } - if newPos == len(filter) { - err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) - return packet, newPos, err - } - if packet == nil { - err = NewError(ErrorFilterCompile, errors.New("ldap: error parsing filter")) - return packet, newPos, err - } - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) - switch { - case packet.Tag == FilterEqualityMatch && condition == "*": - packet.Tag = FilterPresent - packet.Description = FilterMap[uint64(packet.Tag)] - case packet.Tag == FilterEqualityMatch && condition[0] == '*' && condition[len(condition)-1] == '*': - // Any - packet.Tag = FilterSubstrings - packet.Description = FilterMap[uint64(packet.Tag)] - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") - seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsAny, condition[1:len(condition)-1], "Any Substring")) - packet.AppendChild(seq) - case packet.Tag == FilterEqualityMatch && condition[0] == '*': - // Final - packet.Tag = FilterSubstrings - packet.Description = FilterMap[uint64(packet.Tag)] - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") - seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsFinal, condition[1:], "Final Substring")) - packet.AppendChild(seq) - case packet.Tag == FilterEqualityMatch && condition[len(condition)-1] == '*': - // Initial - packet.Tag = FilterSubstrings - packet.Description = FilterMap[uint64(packet.Tag)] - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") - seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsInitial, condition[:len(condition)-1], "Initial Substring")) - packet.AppendChild(seq) - default: - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, condition, "Condition")) - } - newPos++ - return packet, newPos, err - } -} diff --git a/pkg/components/ldap/filter_test.go b/pkg/components/ldap/filter_test.go deleted file mode 100644 index 761ff42fd51..00000000000 --- a/pkg/components/ldap/filter_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package ldap - -import ( - "testing" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -type compileTest struct { - filterStr string - filterType int -} - -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>=Miller)", filterType: FilterGreaterOrEqual}, - compileTest{filterStr: "(sn<=Miller)", filterType: FilterLessOrEqual}, - compileTest{filterStr: "(sn=*)", filterType: FilterPresent}, - compileTest{filterStr: "(sn~=Miller)", filterType: FilterApproxMatch}, - // compileTest{ filterStr: "()", filterType: FilterExtensibleMatch }, -} - -func TestFilter(t *testing.T) { - // Test Compiler and Decompiler - for _, i := range testFilters { - filter, err := CompileFilter(i.filterStr) - if err != nil { - t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) - } else if filter.Tag != uint8(i.filterType) { - t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[uint64(i.filterType)], FilterMap[uint64(filter.Tag)]) - } else { - o, err := 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) - } - } - } -} - -func BenchmarkFilterCompile(b *testing.B) { - b.StopTimer() - filters := make([]string, len(testFilters)) - - // Test Compiler and Decompiler - for idx, i := range testFilters { - filters[idx] = i.filterStr - } - - maxIdx := len(filters) - b.StartTimer() - for i := 0; i < b.N; i++ { - CompileFilter(filters[i%maxIdx]) - } -} - -func BenchmarkFilterDecompile(b *testing.B) { - b.StopTimer() - filters := make([]*ber.Packet, len(testFilters)) - - // Test Compiler and Decompiler - for idx, i := range testFilters { - filters[idx], _ = CompileFilter(i.filterStr) - } - - maxIdx := len(filters) - b.StartTimer() - for i := 0; i < b.N; i++ { - DecompileFilter(filters[i%maxIdx]) - } -} diff --git a/pkg/components/ldap/ldap.go b/pkg/components/ldap/ldap.go deleted file mode 100644 index e990b36231f..00000000000 --- a/pkg/components/ldap/ldap.go +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright 2011 The Go 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 ldap - -import ( - "errors" - "fmt" - "io/ioutil" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -// LDAP Application Codes -const ( - ApplicationBindRequest = 0 - ApplicationBindResponse = 1 - ApplicationUnbindRequest = 2 - ApplicationSearchRequest = 3 - ApplicationSearchResultEntry = 4 - ApplicationSearchResultDone = 5 - ApplicationModifyRequest = 6 - ApplicationModifyResponse = 7 - ApplicationAddRequest = 8 - ApplicationAddResponse = 9 - ApplicationDelRequest = 10 - ApplicationDelResponse = 11 - ApplicationModifyDNRequest = 12 - ApplicationModifyDNResponse = 13 - ApplicationCompareRequest = 14 - ApplicationCompareResponse = 15 - ApplicationAbandonRequest = 16 - ApplicationSearchResultReference = 19 - ApplicationExtendedRequest = 23 - ApplicationExtendedResponse = 24 -) - -var ApplicationMap = map[uint8]string{ - ApplicationBindRequest: "Bind Request", - ApplicationBindResponse: "Bind Response", - ApplicationUnbindRequest: "Unbind Request", - ApplicationSearchRequest: "Search Request", - ApplicationSearchResultEntry: "Search Result Entry", - ApplicationSearchResultDone: "Search Result Done", - ApplicationModifyRequest: "Modify Request", - ApplicationModifyResponse: "Modify Response", - ApplicationAddRequest: "Add Request", - ApplicationAddResponse: "Add Response", - ApplicationDelRequest: "Del Request", - ApplicationDelResponse: "Del Response", - ApplicationModifyDNRequest: "Modify DN Request", - ApplicationModifyDNResponse: "Modify DN Response", - ApplicationCompareRequest: "Compare Request", - ApplicationCompareResponse: "Compare Response", - ApplicationAbandonRequest: "Abandon Request", - ApplicationSearchResultReference: "Search Result Reference", - ApplicationExtendedRequest: "Extended Request", - 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 -) - -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", -} - -// Adds descriptions to an LDAP Response packet for debugging -func addLDAPDescriptions(packet *ber.Packet) (err error) { - defer func() { - if r := recover(); r != nil { - err = NewError(ErrorDebugging, errors.New("ldap: cannot process packet to add descriptions")) - } - }() - packet.Description = "LDAP Response" - packet.Children[0].Description = "Message ID" - - application := packet.Children[1].Tag - packet.Children[1].Description = ApplicationMap[application] - - switch application { - case ApplicationBindRequest: - addRequestDescriptions(packet) - case ApplicationBindResponse: - addDefaultLDAPResponseDescriptions(packet) - case ApplicationUnbindRequest: - addRequestDescriptions(packet) - case ApplicationSearchRequest: - addRequestDescriptions(packet) - case ApplicationSearchResultEntry: - packet.Children[1].Children[0].Description = "Object Name" - packet.Children[1].Children[1].Description = "Attributes" - for _, child := range packet.Children[1].Children[1].Children { - child.Description = "Attribute" - child.Children[0].Description = "Attribute Name" - child.Children[1].Description = "Attribute Values" - for _, grandchild := range child.Children[1].Children { - grandchild.Description = "Attribute Value" - } - } - if len(packet.Children) == 3 { - addControlDescriptions(packet.Children[2]) - } - case ApplicationSearchResultDone: - addDefaultLDAPResponseDescriptions(packet) - case ApplicationModifyRequest: - addRequestDescriptions(packet) - case ApplicationModifyResponse: - case ApplicationAddRequest: - addRequestDescriptions(packet) - case ApplicationAddResponse: - case ApplicationDelRequest: - addRequestDescriptions(packet) - case ApplicationDelResponse: - case ApplicationModifyDNRequest: - addRequestDescriptions(packet) - case ApplicationModifyDNResponse: - case ApplicationCompareRequest: - addRequestDescriptions(packet) - case ApplicationCompareResponse: - case ApplicationAbandonRequest: - addRequestDescriptions(packet) - case ApplicationSearchResultReference: - case ApplicationExtendedRequest: - addRequestDescriptions(packet) - case ApplicationExtendedResponse: - } - - return nil -} - -func addControlDescriptions(packet *ber.Packet) { - packet.Description = "Controls" - for _, child := range packet.Children { - child.Description = "Control" - child.Children[0].Description = "Control Type (" + ControlTypeMap[child.Children[0].Value.(string)] + ")" - value := child.Children[1] - if len(child.Children) == 3 { - child.Children[1].Description = "Criticality" - value = child.Children[2] - } - value.Description = "Control Value" - - switch child.Children[0].Value.(string) { - case ControlTypePaging: - value.Description += " (Paging)" - if value.Value != nil { - valueChildren := ber.DecodePacket(value.Data.Bytes()) - value.Data.Truncate(0) - value.Value = nil - valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes() - value.AppendChild(valueChildren) - } - value.Children[0].Description = "Real Search Control Value" - value.Children[0].Children[0].Description = "Paging Size" - value.Children[0].Children[1].Description = "Cookie" - } - } -} - -func addRequestDescriptions(packet *ber.Packet) { - packet.Description = "LDAP Request" - packet.Children[0].Description = "Message ID" - packet.Children[1].Description = ApplicationMap[packet.Children[1].Tag] - if len(packet.Children) == 3 { - addControlDescriptions(packet.Children[2]) - } -} - -func addDefaultLDAPResponseDescriptions(packet *ber.Packet) { - resultCode := packet.Children[1].Children[0].Value.(uint64) - packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[uint8(resultCode)] + ")" - packet.Children[1].Children[1].Description = "Matched DN" - packet.Children[1].Children[2].Description = "Error Message" - if len(packet.Children[1].Children) > 3 { - packet.Children[1].Children[3].Description = "Referral" - } - if len(packet.Children) == 3 { - addControlDescriptions(packet.Children[2]) - } -} - -func DebugBinaryFile(fileName string) error { - file, err := ioutil.ReadFile(fileName) - if err != nil { - return NewError(ErrorDebugging, err) - } - ber.PrintBytes(file, "") - packet := ber.DecodePacket(file) - addLDAPDescriptions(packet) - ber.PrintPacket(packet) - - 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.(uint64)), response.Children[2].Value.(string) - } - } - - return ErrorNetwork, "Invalid packet format" -} diff --git a/pkg/components/ldap/ldap_test.go b/pkg/components/ldap/ldap_test.go deleted file mode 100644 index 31cfbf02f1b..00000000000 --- a/pkg/components/ldap/ldap_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package ldap - -import ( - "fmt" - "testing" -) - -var ldapServer = "ldap.itd.umich.edu" -var ldapPort = uint16(389) -var baseDN = "dc=umich,dc=edu" -var filter = []string{ - "(cn=cis-fac)", - "(&(objectclass=rfc822mailgroup)(cn=*Computer*))", - "(&(objectclass=rfc822mailgroup)(cn=*Mathematics*))"} -var attributes = []string{ - "cn", - "description"} - -func TestConnect(t *testing.T) { - fmt.Printf("TestConnect: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) - if err != nil { - t.Errorf(err.Error()) - return - } - defer l.Close() - fmt.Printf("TestConnect: finished...\n") -} - -func TestSearch(t *testing.T) { - fmt.Printf("TestSearch: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) - if err != nil { - t.Errorf(err.Error()) - return - } - defer l.Close() - - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[0], - attributes, - nil) - - sr, err := l.Search(searchRequest) - if err != nil { - t.Errorf(err.Error()) - return - } - - fmt.Printf("TestSearch: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) -} - -func TestSearchWithPaging(t *testing.T) { - fmt.Printf("TestSearchWithPaging: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) - if err != nil { - t.Errorf(err.Error()) - return - } - defer l.Close() - - err = l.Bind("", "") - if err != nil { - t.Errorf(err.Error()) - return - } - - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[1], - attributes, - nil) - 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)) -} - -func testMultiGoroutineSearch(t *testing.T, l *Conn, results chan *SearchResult, i int) { - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[i], - attributes, - nil) - sr, err := l.Search(searchRequest) - if err != nil { - t.Errorf(err.Error()) - results <- nil - return - } - results <- sr -} - -func TestMultiGoroutineSearch(t *testing.T) { - fmt.Printf("TestMultiGoroutineSearch: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) - if err != nil { - t.Errorf(err.Error()) - return - } - defer l.Close() - - results := make([]chan *SearchResult, len(filter)) - for i := range filter { - results[i] = make(chan *SearchResult) - go testMultiGoroutineSearch(t, l, results[i], i) - } - for i := range filter { - sr := <-results[i] - if sr == nil { - t.Errorf("Did not receive results from goroutine for %q", filter[i]) - } else { - fmt.Printf("TestMultiGoroutineSearch(%d): %s -> num of entries = %d\n", i, filter[i], len(sr.Entries)) - } - } -} diff --git a/pkg/components/ldap/modify.go b/pkg/components/ldap/modify.go deleted file mode 100644 index decc1eddca0..00000000000 --- a/pkg/components/ldap/modify.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2014 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// -// File contains Modify functionality -// -// https://tools.ietf.org/html/rfc4511 -// -// ModifyRequest ::= [APPLICATION 6] SEQUENCE { -// object LDAPDN, -// changes SEQUENCE OF change SEQUENCE { -// operation ENUMERATED { -// add (0), -// delete (1), -// replace (2), -// ... }, -// modification PartialAttribute } } -// -// PartialAttribute ::= SEQUENCE { -// type AttributeDescription, -// vals SET OF value AttributeValue } -// -// AttributeDescription ::= LDAPString -// -- Constrained to -// -- [RFC4512] -// -// AttributeValue ::= OCTET STRING -// - -package ldap - -import ( - "errors" - "log" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -const ( - AddAttribute = 0 - DeleteAttribute = 1 - ReplaceAttribute = 2 -) - -type PartialAttribute struct { - attrType string - attrVals []string -} - -func (p *PartialAttribute) encode() *ber.Packet { - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PartialAttribute") - seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, p.attrType, "Type")) - set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") - for _, value := range p.attrVals { - set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) - } - seq.AppendChild(set) - return seq -} - -type ModifyRequest struct { - dn string - addAttributes []PartialAttribute - deleteAttributes []PartialAttribute - replaceAttributes []PartialAttribute -} - -func (m *ModifyRequest) Add(attrType string, attrVals []string) { - m.addAttributes = append(m.addAttributes, PartialAttribute{attrType: attrType, attrVals: attrVals}) -} - -func (m *ModifyRequest) Delete(attrType string, attrVals []string) { - m.deleteAttributes = append(m.deleteAttributes, PartialAttribute{attrType: attrType, attrVals: attrVals}) -} - -func (m *ModifyRequest) Replace(attrType string, attrVals []string) { - m.replaceAttributes = append(m.replaceAttributes, PartialAttribute{attrType: attrType, attrVals: attrVals}) -} - -func (m ModifyRequest) encode() *ber.Packet { - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyRequest, nil, "Modify Request") - request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, m.dn, "DN")) - changes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Changes") - for _, attribute := range m.addAttributes { - change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") - change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(AddAttribute), "Operation")) - change.AppendChild(attribute.encode()) - changes.AppendChild(change) - } - for _, attribute := range m.deleteAttributes { - change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") - change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(DeleteAttribute), "Operation")) - change.AppendChild(attribute.encode()) - changes.AppendChild(change) - } - for _, attribute := range m.replaceAttributes { - change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") - change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ReplaceAttribute), "Operation")) - change.AppendChild(attribute.encode()) - changes.AppendChild(change) - } - request.AppendChild(changes) - return request -} - -func NewModifyRequest( - dn string, -) *ModifyRequest { - return &ModifyRequest{ - dn: dn, - } -} - -func (l *Conn) Modify(modifyRequest *ModifyRequest) 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(modifyRequest.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 == ApplicationModifyResponse { - 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/pkg/components/ldap/search.go b/pkg/components/ldap/search.go deleted file mode 100644 index e2a62064468..00000000000 --- a/pkg/components/ldap/search.go +++ /dev/null @@ -1,350 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// -// File contains Search functionality -// -// https://tools.ietf.org/html/rfc4511 -// -// SearchRequest ::= [APPLICATION 3] SEQUENCE { -// baseObject LDAPDN, -// scope ENUMERATED { -// baseObject (0), -// singleLevel (1), -// wholeSubtree (2), -// ... }, -// derefAliases ENUMERATED { -// neverDerefAliases (0), -// derefInSearching (1), -// derefFindingBaseObj (2), -// derefAlways (3) }, -// sizeLimit INTEGER (0 .. maxInt), -// timeLimit INTEGER (0 .. maxInt), -// typesOnly BOOLEAN, -// filter Filter, -// attributes AttributeSelection } -// -// AttributeSelection ::= SEQUENCE OF selector LDAPString -// -- The LDAPString is constrained to -// -- in Section 4.5.1.8 -// -// Filter ::= CHOICE { -// and [0] SET SIZE (1..MAX) OF filter Filter, -// or [1] SET SIZE (1..MAX) OF filter Filter, -// not [2] Filter, -// equalityMatch [3] AttributeValueAssertion, -// substrings [4] SubstringFilter, -// greaterOrEqual [5] AttributeValueAssertion, -// lessOrEqual [6] AttributeValueAssertion, -// present [7] AttributeDescription, -// approxMatch [8] AttributeValueAssertion, -// extensibleMatch [9] MatchingRuleAssertion, -// ... } -// -// SubstringFilter ::= SEQUENCE { -// type AttributeDescription, -// substrings SEQUENCE SIZE (1..MAX) OF substring CHOICE { -// initial [0] AssertionValue, -- can occur at most once -// any [1] AssertionValue, -// final [2] AssertionValue } -- can occur at most once -// } -// -// MatchingRuleAssertion ::= SEQUENCE { -// matchingRule [1] MatchingRuleId OPTIONAL, -// type [2] AttributeDescription OPTIONAL, -// matchValue [3] AssertionValue, -// dnAttributes [4] BOOLEAN DEFAULT FALSE } -// -// - -package ldap - -import ( - "errors" - "fmt" - "strings" - - "github.com/gogits/gogs/modules/asn1-ber" -) - -const ( - ScopeBaseObject = 0 - ScopeSingleLevel = 1 - ScopeWholeSubtree = 2 -) - -var ScopeMap = map[int]string{ - ScopeBaseObject: "Base Object", - ScopeSingleLevel: "Single Level", - ScopeWholeSubtree: "Whole Subtree", -} - -const ( - NeverDerefAliases = 0 - DerefInSearching = 1 - DerefFindingBaseObj = 2 - DerefAlways = 3 -) - -var DerefMap = map[int]string{ - NeverDerefAliases: "NeverDerefAliases", - DerefInSearching: "DerefInSearching", - DerefFindingBaseObj: "DerefFindingBaseObj", - DerefAlways: "DerefAlways", -} - -type Entry struct { - DN string - Attributes []*EntryAttribute -} - -func (e *Entry) GetAttributeValues(attribute string) []string { - for _, attr := range e.Attributes { - if attr.Name == attribute { - return attr.Values - } - } - return []string{} -} - -func (e *Entry) GetAttributeValue(attribute string) string { - values := e.GetAttributeValues(attribute) - if len(values) == 0 { - return "" - } - return values[0] -} - -func (e *Entry) Print() { - fmt.Printf("DN: %s\n", e.DN) - for _, attr := range e.Attributes { - attr.Print() - } -} - -func (e *Entry) PrettyPrint(indent int) { - fmt.Printf("%sDN: %s\n", strings.Repeat(" ", indent), e.DN) - for _, attr := range e.Attributes { - attr.PrettyPrint(indent + 2) - } -} - -type EntryAttribute struct { - Name string - Values []string -} - -func (e *EntryAttribute) Print() { - fmt.Printf("%s: %s\n", e.Name, e.Values) -} - -func (e *EntryAttribute) PrettyPrint(indent int) { - fmt.Printf("%s%s: %s\n", strings.Repeat(" ", indent), e.Name, e.Values) -} - -type SearchResult struct { - Entries []*Entry - Referrals []string - Controls []Control -} - -func (s *SearchResult) Print() { - for _, entry := range s.Entries { - entry.Print() - } -} - -func (s *SearchResult) PrettyPrint(indent int) { - for _, entry := range s.Entries { - entry.PrettyPrint(indent) - } -} - -type SearchRequest struct { - BaseDN string - Scope int - DerefAliases int - SizeLimit int - TimeLimit int - TypesOnly bool - Filter string - Attributes []string - Controls []Control -} - -func (s *SearchRequest) encode() (*ber.Packet, error) { - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request") - request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, s.BaseDN, "Base DN")) - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(s.Scope), "Scope")) - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(s.DerefAliases), "Deref Aliases")) - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(s.SizeLimit), "Size Limit")) - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(s.TimeLimit), "Time Limit")) - request.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, s.TypesOnly, "Types Only")) - // compile and encode filter - filterPacket, err := CompileFilter(s.Filter) - if err != nil { - return nil, err - } - request.AppendChild(filterPacket) - // encode attributes - attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") - for _, attribute := range s.Attributes { - attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) - } - request.AppendChild(attributesPacket) - return request, nil -} - -func NewSearchRequest( - BaseDN string, - Scope, DerefAliases, SizeLimit, TimeLimit int, - TypesOnly bool, - Filter string, - Attributes []string, - Controls []Control, -) *SearchRequest { - return &SearchRequest{ - BaseDN: BaseDN, - Scope: Scope, - DerefAliases: DerefAliases, - SizeLimit: SizeLimit, - TimeLimit: TimeLimit, - TypesOnly: TypesOnly, - Filter: Filter, - Attributes: Attributes, - Controls: Controls, - } -} - -func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) { - if searchRequest.Controls == nil { - searchRequest.Controls = make([]Control, 0) - } - - pagingControl := NewControlPaging(pagingSize) - searchRequest.Controls = append(searchRequest.Controls, pagingControl) - searchResult := new(SearchResult) - for { - result, err := l.Search(searchRequest) - l.Debug.Printf("Looking for Paging Control...") - if err != nil { - return searchResult, err - } - if result == nil { - return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) - } - - for _, entry := range result.Entries { - searchResult.Entries = append(searchResult.Entries, entry) - } - for _, referral := range result.Referrals { - searchResult.Referrals = append(searchResult.Referrals, referral) - } - for _, control := range result.Controls { - searchResult.Controls = append(searchResult.Controls, control) - } - - l.Debug.Printf("Looking for Paging Control...") - pagingResult := FindControl(result.Controls, ControlTypePaging) - if pagingResult == nil { - pagingControl = nil - l.Debug.Printf("Could not find paging control. Breaking...") - break - } - - cookie := pagingResult.(*ControlPaging).Cookie - if len(cookie) == 0 { - pagingControl = nil - l.Debug.Printf("Could not find cookie. Breaking...") - break - } - pagingControl.SetCookie(cookie) - } - - if pagingControl != nil { - l.Debug.Printf("Abandoning Paging...") - pagingControl.PagingSize = 0 - l.Search(searchRequest) - } - - return searchResult, nil -} - -func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, 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")) - // encode search request - encodedSearchRequest, err := searchRequest.encode() - if err != nil { - return nil, err - } - packet.AppendChild(encodedSearchRequest) - // encode search controls - if searchRequest.Controls != nil { - packet.AppendChild(encodeControls(searchRequest.Controls)) - } - - l.Debug.PrintPacket(packet) - - channel, err := l.sendMessage(packet) - if err != nil { - return nil, err - } - if channel == nil { - return nil, NewError(ErrorNetwork, errors.New("ldap: could not send message")) - } - defer l.finishMessage(messageID) - - result := &SearchResult{ - Entries: make([]*Entry, 0), - Referrals: make([]string, 0), - Controls: make([]Control, 0)} - - foundSearchResultDone := false - for !foundSearchResultDone { - l.Debug.Printf("%d: waiting for response", messageID) - packet = <-channel - l.Debug.Printf("%d: got response %p", messageID, packet) - if packet == nil { - return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) - } - - if l.Debug { - if err := addLDAPDescriptions(packet); err != nil { - return nil, err - } - ber.PrintPacket(packet) - } - - switch packet.Children[1].Tag { - case 4: - entry := new(Entry) - entry.DN = packet.Children[1].Children[0].Value.(string) - for _, child := range packet.Children[1].Children[1].Children { - attr := new(EntryAttribute) - attr.Name = child.Children[0].Value.(string) - for _, value := range child.Children[1].Children { - attr.Values = append(attr.Values, value.Value.(string)) - } - entry.Attributes = append(entry.Attributes, attr) - } - result.Entries = append(result.Entries, entry) - case 5: - resultCode, resultDescription := getLDAPResultCode(packet) - if resultCode != 0 { - return result, NewError(resultCode, errors.New(resultDescription)) - } - if len(packet.Children) == 3 { - for _, child := range packet.Children[2].Children { - result.Controls = append(result.Controls, DecodeControl(child)) - } - } - foundSearchResultDone = true - case 19: - result.Referrals = append(result.Referrals, packet.Children[1].Children[0].Value.(string)) - } - } - l.Debug.Printf("%d: returning", messageID) - return result, nil -} diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index 9d5ddd00d73..ec11a9ac36f 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -54,7 +54,7 @@ func RenderToPng(params *RenderOpts) (string, error) { }() select { - case <-time.After(10 * time.Second): + case <-time.After(15 * time.Second): if err := cmd.Process.Kill(); err != nil { log.Error(4, "failed to kill: %v", err) } diff --git a/pkg/events/events.go b/pkg/events/events.go index c3dcac3e2b5..5e82578b474 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -5,7 +5,7 @@ import ( "time" ) -// Events can be passed to external systems via for example AMPQ +// Events can be passed to external systems via for example AMQP // Treat these events as basically DTOs so changes has to be backward compatible type Priority string @@ -70,6 +70,14 @@ type UserCreated struct { Email string `json:"email"` } +type UserSignedUp struct { + Timestamp time.Time `json:"timestamp"` + Id int64 `json:"id"` + Name string `json:"name"` + Login string `json:"login"` + Email string `json:"email"` +} + type UserUpdated struct { Timestamp time.Time `json:"timestamp"` Id int64 `json:"id"` diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index 42cd91a3ad1..2529fef67a9 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -60,8 +60,10 @@ func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand { cmd := m.CreateUserCommand{} if setting.AuthProxyHeaderProperty == "username" { cmd.Login = headerVal + cmd.Email = headerVal } else if setting.AuthProxyHeaderProperty == "email" { cmd.Email = headerVal + cmd.Login = headerVal } else { panic("Auth proxy header property invalid") } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 2a6873076c6..8704ec5a787 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) type Context struct { @@ -40,6 +41,7 @@ func GetContextHandler() macaron.Handler { // then look for api key in session (special case for render calls via api) // then test if anonymous access is enabled if initContextWithApiKey(ctx) || + initContextWithBasicAuth(ctx) || initContextWithAuthProxy(ctx) || initContextWithUserSessionCookie(ctx) || initContextWithApiKeyFromSession(ctx) || @@ -128,6 +130,47 @@ func initContextWithApiKey(ctx *Context) bool { } } +func initContextWithBasicAuth(ctx *Context) bool { + if !setting.BasicAuthEnabled { + return false + } + + header := ctx.Req.Header.Get("Authorization") + if header == "" { + return false + } + + username, password, err := util.DecodeBasicAuthHeader(header) + if err != nil { + ctx.JsonApiErr(401, "Invalid Basic Auth Header", err) + return true + } + + loginQuery := m.GetUserByLoginQuery{LoginOrEmail: username} + if err := bus.Dispatch(&loginQuery); err != nil { + ctx.JsonApiErr(401, "Basic auth failed", err) + return true + } + + user := loginQuery.Result + + // validate password + if util.EncodePassword(password, user.Salt) != user.Password { + ctx.JsonApiErr(401, "Invalid username or password", nil) + return true + } + + query := m.GetSignedInUserQuery{UserId: user.Id} + if err := bus.Dispatch(&query); err != nil { + ctx.JsonApiErr(401, "Authentication error", err) + return true + } else { + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + return true + } +} + // special case for panel render calls with api key func initContextWithApiKeyFromSession(ctx *Context) bool { keyId := ctx.Session.Get(SESS_KEY_APIKEY) @@ -197,10 +240,10 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) { switch status { case 404: - resp["message"] = "Not Found" - metrics.M_Api_Status_500.Inc(1) - case 500: metrics.M_Api_Status_404.Inc(1) + resp["message"] = "Not Found" + case 500: + metrics.M_Api_Status_500.Inc(1) resp["message"] = "Internal Server Error" } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 212e250cc1c..97d369d00cf 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -48,6 +48,32 @@ func TestMiddlewareContext(t *testing.T) { }) }) + middlewareScenario("Using basic auth", func(sc *scenarioContext) { + + bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error { + query.Result = &m.User{ + Password: util.EncodePassword("myPass", "salt"), + Salt: "salt", + } + return nil + }) + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + setting.BasicAuthEnabled = true + authHeader := util.GetBasicAuthHeader("myUser", "myPass") + sc.fakeReq("GET", "/").withAuthoriziationHeader(authHeader).exec() + + Convey("Should init middleware context with user", func() { + So(sc.context.IsSignedIn, ShouldEqual, true) + So(sc.context.OrgId, ShouldEqual, 2) + So(sc.context.UserId, ShouldEqual, 12) + }) + }) + middlewareScenario("Valid api key", func(sc *scenarioContext) { keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") @@ -223,6 +249,7 @@ type scenarioContext struct { context *Context resp *httptest.ResponseRecorder apiKey string + authHeader string respJson map[string]interface{} handlerFunc handlerFunc defaultHandler macaron.Handler @@ -240,6 +267,11 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext { return sc } +func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext { + sc.authHeader = authHeader + return sc +} + func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { sc.resp = httptest.NewRecorder() req, err := http.NewRequest(method, url, nil) @@ -266,6 +298,10 @@ func (sc *scenarioContext) exec() { sc.req.Header.Add("Authorization", "Bearer "+sc.apiKey) } + if sc.authHeader != "" { + sc.req.Header.Add("Authorization", sc.authHeader) + } + sc.m.ServeHTTP(sc.resp, sc.req) if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index b802eba7e1e..7d4a2690556 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -5,12 +5,13 @@ import ( "strings" "time" - "github.com/dalu/slug" + "github.com/gosimple/slug" ) // Typed errors var ( ErrDashboardNotFound = errors.New("Dashboard not found") + ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ) @@ -49,7 +50,7 @@ func NewDashboard(title string) *Dashboard { // GetTags turns the tags in data json into go string array func (dash *Dashboard) GetTags() []string { jsonTags := dash.Data["tags"] - if jsonTags == nil { + if jsonTags == nil || jsonTags == "" { return []string{} } diff --git a/pkg/models/dashboard_test.go b/pkg/models/dashboards_test.go similarity index 53% rename from pkg/models/dashboard_test.go rename to pkg/models/dashboards_test.go index 0828e51480f..b0b6796c4d8 100644 --- a/pkg/models/dashboard_test.go +++ b/pkg/models/dashboards_test.go @@ -15,4 +15,17 @@ func TestDashboardModel(t *testing.T) { So(dashboard.Slug, ShouldEqual, "grafana-play-home") }) + Convey("Given a dashboard json", t, func() { + json := map[string]interface{}{ + "title": "test dash", + } + + Convey("With tags as string value", func() { + json["tags"] = "" + dash := NewDashboardFromJson(json) + + So(len(dash.GetTags()), ShouldEqual, 0) + }) + }) + } diff --git a/pkg/models/emails.go b/pkg/models/emails.go new file mode 100644 index 00000000000..74da180f7d8 --- /dev/null +++ b/pkg/models/emails.go @@ -0,0 +1,22 @@ +package models + +import "errors" + +var ErrInvalidEmailCode = errors.New("Invalid or expired email code") + +type SendEmailCommand struct { + To []string + Template string + Data map[string]interface{} + Massive bool + Info string +} + +type SendResetPasswordEmailCommand struct { + User *User +} + +type ValidateResetPasswordCodeQuery struct { + Code string + Result *User +} diff --git a/pkg/models/models.go b/pkg/models/models.go index c38f0c5a391..189e594576b 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -1,7 +1,5 @@ package models -import "errors" - type OAuthType int const ( @@ -9,5 +7,3 @@ const ( GOOGLE TWITTER ) - -var ErrNotFound = errors.New("Not found") diff --git a/pkg/models/user.go b/pkg/models/user.go index 5efecc8deef..bf697676b32 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -30,6 +30,16 @@ type User struct { Updated time.Time } +func (u *User) NameOrFallback() string { + if u.Name != "" { + return u.Name + } else if u.Login != "" { + return u.Login + } else { + return u.Email + } +} + // --------------------- // COMMANDS diff --git a/pkg/services/notifications/codes.go b/pkg/services/notifications/codes.go new file mode 100644 index 00000000000..4dbe76c1cad --- /dev/null +++ b/pkg/services/notifications/codes.go @@ -0,0 +1,98 @@ +package notifications + +import ( + "crypto/sha1" + "encoding/hex" + "fmt" + "time" + + "github.com/Unknwon/com" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +const timeLimitCodeLength = 12 + 6 + 40 + +// create a time limit code +// code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string +func createTimeLimitCode(data string, minutes int, startInf interface{}) string { + format := "200601021504" + + var start, end time.Time + var startStr, endStr string + + if startInf == nil { + // Use now time create code + start = time.Now() + startStr = start.Format(format) + } else { + // use start string create code + startStr = startInf.(string) + start, _ = time.ParseInLocation(format, startStr, time.Local) + startStr = start.Format(format) + } + + end = start.Add(time.Minute * time.Duration(minutes)) + endStr = end.Format(format) + + // create sha1 encode string + sh := sha1.New() + sh.Write([]byte(data + setting.SecretKey + startStr + endStr + com.ToStr(minutes))) + encoded := hex.EncodeToString(sh.Sum(nil)) + + code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded) + return code +} + +// verify time limit code +func validateUserEmailCode(user *m.User, code string) bool { + if len(code) <= 18 { + return false + } + + minutes := setting.EmailCodeValidMinutes + code = code[:timeLimitCodeLength] + + // split code + start := code[:12] + lives := code[12:18] + if d, err := com.StrTo(lives).Int(); err == nil { + minutes = d + } + + // right active code + data := com.ToStr(user.Id) + user.Email + user.Login + user.Password + user.Rands + retCode := createTimeLimitCode(data, minutes, start) + fmt.Printf("code : %s\ncode2: %s", retCode, code) + if retCode == code && minutes > 0 { + // check time is expired or not + before, _ := time.ParseInLocation("200601021504", start, time.Local) + now := time.Now() + if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() { + return true + } + } + + return false +} + +func getLoginForEmailCode(code string) string { + if len(code) <= timeLimitCodeLength { + return "" + } + + // use tail hex username query user + hexStr := code[timeLimitCodeLength:] + b, _ := hex.DecodeString(hexStr) + return string(b) +} + +func createUserEmailCode(u *m.User, startInf interface{}) string { + minutes := setting.EmailCodeValidMinutes + data := com.ToStr(u.Id) + u.Email + u.Login + u.Password + u.Rands + code := createTimeLimitCode(data, minutes, startInf) + + // add tail hex username + code += hex.EncodeToString([]byte(u.Login)) + return code +} diff --git a/pkg/services/notifications/codes_test.go b/pkg/services/notifications/codes_test.go new file mode 100644 index 00000000000..be1fc91153b --- /dev/null +++ b/pkg/services/notifications/codes_test.go @@ -0,0 +1,35 @@ +package notifications + +import ( + "testing" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestEmailCodes(t *testing.T) { + + Convey("When generating code", t, func() { + setting.EmailCodeValidMinutes = 120 + + user := &m.User{Id: 10, Email: "t@a.com", Login: "asd", Password: "1", Rands: "2"} + code := createUserEmailCode(user, nil) + + Convey("getLoginForCode should return login", func() { + login := getLoginForEmailCode(code) + So(login, ShouldEqual, "asd") + }) + + Convey("Can verify valid code", func() { + So(validateUserEmailCode(user, code), ShouldBeTrue) + }) + + Convey("Cannot verify in-valid code", func() { + code = "ASD" + So(validateUserEmailCode(user, code), ShouldBeFalse) + }) + + }) + +} diff --git a/pkg/services/notifications/email.go b/pkg/services/notifications/email.go new file mode 100644 index 00000000000..f81f3e1007b --- /dev/null +++ b/pkg/services/notifications/email.go @@ -0,0 +1,33 @@ +package notifications + +import ( + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +type Message struct { + To []string + From string + Subject string + Body string + Massive bool + Info string +} + +// create mail content +func (m *Message) Content() string { + contentType := "text/html; charset=UTF-8" + content := "From: " + m.From + "\r\nSubject: " + m.Subject + "\r\nContent-Type: " + contentType + "\r\n\r\n" + m.Body + return content +} + +func setDefaultTemplateData(data map[string]interface{}, u *m.User) { + data["AppUrl"] = setting.AppUrl + data["BuildVersion"] = setting.BuildVersion + data["BuildStamp"] = setting.BuildStamp + data["EmailCodeValidHours"] = setting.EmailCodeValidMinutes / 60 + data["Subject"] = map[string]interface{}{} + if u != nil { + data["Name"] = u.NameOrFallback() + } +} diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go new file mode 100644 index 00000000000..309436cb7d9 --- /dev/null +++ b/pkg/services/notifications/mailer.go @@ -0,0 +1,186 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package notifications + +import ( + "crypto/tls" + "fmt" + "net" + "net/mail" + "net/smtp" + "os" + "strings" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" +) + +var mailQueue chan *Message + +func initMailQueue() { + mailQueue = make(chan *Message, 10) + go processMailQueue() +} + +func processMailQueue() { + for { + select { + case msg := <-mailQueue: + num, err := buildAndSend(msg) + tos := strings.Join(msg.To, "; ") + info := "" + if err != nil { + if len(msg.Info) > 0 { + info = ", info: " + msg.Info + } + log.Error(4, fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err)) + } else { + log.Trace(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info)) + } + } + } +} + +var addToMailQueue = func(msg *Message) { + mailQueue <- msg +} + +func sendToSmtpServer(recipients []string, msgContent []byte) error { + host, port, err := net.SplitHostPort(setting.Smtp.Host) + if err != nil { + return err + } + + tlsconfig := &tls.Config{ + InsecureSkipVerify: setting.Smtp.SkipVerify, + ServerName: host, + } + + if setting.Smtp.CertFile != "" { + cert, err := tls.LoadX509KeyPair(setting.Smtp.CertFile, setting.Smtp.KeyFile) + if err != nil { + return err + } + tlsconfig.Certificates = []tls.Certificate{cert} + } + + conn, err := net.Dial("tcp", net.JoinHostPort(host, port)) + if err != nil { + return err + } + defer conn.Close() + + isSecureConn := false + // Start TLS directly if the port ends with 465 (SMTPS protocol) + if strings.HasSuffix(port, "465") { + conn = tls.Client(conn, tlsconfig) + isSecureConn = true + } + + client, err := smtp.NewClient(conn, host) + if err != nil { + return err + } + + hostname, err := os.Hostname() + if err != nil { + return err + } + + if err = client.Hello(hostname); err != nil { + return err + } + + // If not using SMTPS, alway use STARTTLS if available + hasStartTLS, _ := client.Extension("STARTTLS") + if !isSecureConn && hasStartTLS { + if err = client.StartTLS(tlsconfig); err != nil { + return err + } + } + + canAuth, options := client.Extension("AUTH") + + if canAuth && len(setting.Smtp.User) > 0 { + var auth smtp.Auth + + if strings.Contains(options, "CRAM-MD5") { + auth = smtp.CRAMMD5Auth(setting.Smtp.User, setting.Smtp.Password) + } else if strings.Contains(options, "PLAIN") { + auth = smtp.PlainAuth("", setting.Smtp.User, setting.Smtp.Password, host) + } + + if auth != nil { + if err = client.Auth(auth); err != nil { + return err + } + } + } + + if fromAddress, err := mail.ParseAddress(setting.Smtp.FromAddress); err != nil { + return err + } else { + if err = client.Mail(fromAddress.Address); err != nil { + return err + } + } + + for _, rec := range recipients { + if err = client.Rcpt(rec); err != nil { + return err + } + } + + w, err := client.Data() + if err != nil { + return err + } + if _, err = w.Write([]byte(msgContent)); err != nil { + return err + } + + if err = w.Close(); err != nil { + return err + } + + return client.Quit() +} + +func buildAndSend(msg *Message) (int, error) { + log.Trace("Sending mails to: %s", strings.Join(msg.To, "; ")) + + // get message body + content := msg.Content() + + if len(msg.To) == 0 { + return 0, fmt.Errorf("empty receive emails") + } else if len(msg.Body) == 0 { + return 0, fmt.Errorf("empty email body") + } + + if msg.Massive { + // send mail to multiple emails one by one + num := 0 + for _, to := range msg.To { + body := []byte("To: " + to + "\r\n" + content) + err := sendToSmtpServer([]string{to}, body) + if err != nil { + return num, err + } + num++ + } + return num, nil + } else { + body := []byte("To: " + strings.Join(msg.To, ";") + "\r\n" + content) + + // send to multiple emails in one message + err := sendToSmtpServer(msg.To, body) + if err != nil { + return 0, err + } else { + return 1, nil + } + } +} diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go new file mode 100644 index 00000000000..401cd812d5e --- /dev/null +++ b/pkg/services/notifications/notifications.go @@ -0,0 +1,137 @@ +package notifications + +import ( + "bytes" + "errors" + "html/template" + "path/filepath" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/events" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" +) + +var mailTemplates *template.Template +var tmplResetPassword = "reset_password.html" +var tmplWelcomeOnSignUp = "welcome_on_signup.html" + +func Init() error { + initMailQueue() + + bus.AddHandler("email", sendResetPasswordEmail) + bus.AddHandler("email", validateResetPasswordCode) + bus.AddHandler("email", sendEmailCommandHandler) + + bus.AddEventListener(userSignedUpHandler) + + mailTemplates = template.New("name") + mailTemplates.Funcs(template.FuncMap{ + "Subject": subjectTemplateFunc, + }) + + templatePattern := filepath.Join(setting.StaticRootPath, setting.Smtp.TemplatesPattern) + _, err := mailTemplates.ParseGlob(templatePattern) + if err != nil { + return err + } + + if !util.IsEmail(setting.Smtp.FromAddress) { + return errors.New("Invalid email address for smpt from_adress config") + } + + if setting.EmailCodeValidMinutes == 0 { + setting.EmailCodeValidMinutes = 120 + } + + return nil +} + +func subjectTemplateFunc(obj map[string]interface{}, value string) string { + obj["value"] = value + return "" +} + +func sendEmailCommandHandler(cmd *m.SendEmailCommand) error { + if !setting.Smtp.Enabled { + return errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin") + } + + var buffer bytes.Buffer + data := cmd.Data + if data == nil { + data = make(map[string]interface{}, 10) + } + + setDefaultTemplateData(data, nil) + mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data) + + subjectTmplText := data["Subject"].(map[string]interface{})["value"].(string) + subjectTmpl, err := template.New("subject").Parse(subjectTmplText) + if err != nil { + return err + } + + var subjectBuffer bytes.Buffer + err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data) + if err != nil { + return err + } + + addToMailQueue(&Message{ + To: cmd.To, + From: setting.Smtp.FromAddress, + Subject: subjectBuffer.String(), + Body: buffer.String(), + }) + + return nil +} + +func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error { + return sendEmailCommandHandler(&m.SendEmailCommand{ + To: []string{cmd.User.Email}, + Template: tmplResetPassword, + Data: map[string]interface{}{ + "Code": createUserEmailCode(cmd.User, nil), + "Name": cmd.User.NameOrFallback(), + }, + }) +} + +func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error { + login := getLoginForEmailCode(query.Code) + if login == "" { + return m.ErrInvalidEmailCode + } + + userQuery := m.GetUserByLoginQuery{LoginOrEmail: login} + if err := bus.Dispatch(&userQuery); err != nil { + return err + } + + if !validateUserEmailCode(userQuery.Result, query.Code) { + return m.ErrInvalidEmailCode + } + + query.Result = userQuery.Result + return nil +} + +func userSignedUpHandler(evt *events.UserSignedUp) error { + log.Info("User signed up: %s, send_option: %s", evt.Email, setting.Smtp.SendWelcomeEmailOnSignUp) + + if evt.Email == "" || !setting.Smtp.SendWelcomeEmailOnSignUp { + return nil + } + + return sendEmailCommandHandler(&m.SendEmailCommand{ + To: []string{evt.Email}, + Template: tmplWelcomeOnSignUp, + Data: map[string]interface{}{ + "Name": evt.Login, + }, + }) +} diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go new file mode 100644 index 00000000000..110e24cb810 --- /dev/null +++ b/pkg/services/notifications/notifications_test.go @@ -0,0 +1,39 @@ +package notifications + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestNotifications(t *testing.T) { + + Convey("Given the notifications service", t, func() { + bus.ClearBusHandlers() + + setting.StaticRootPath = "../../../public/" + setting.Smtp.Enabled = true + setting.Smtp.TemplatesPattern = "emails/*.html" + setting.Smtp.FromAddress = "from@address.com" + + err := Init() + So(err, ShouldBeNil) + + var sentMsg *Message + addToMailQueue = func(msg *Message) { + sentMsg = msg + } + + Convey("When sending reset email password", func() { + err := sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}}) + So(err, ShouldBeNil) + So(sentMsg.Body, ShouldContainSubstring, "body") + So(sentMsg.Subject, ShouldEqual, "Reset your Grafana password - asd@asd.com") + So(sentMsg.Body, ShouldNotContainSubstring, "Subject") + }) + }) + +} diff --git a/pkg/search/handlers.go b/pkg/services/search/handlers.go similarity index 96% rename from pkg/search/handlers.go rename to pkg/services/search/handlers.go index 326924f05f4..1c480992cbc 100644 --- a/pkg/search/handlers.go +++ b/pkg/services/search/handlers.go @@ -1,6 +1,7 @@ package search import ( + "log" "path/filepath" "sort" @@ -15,6 +16,12 @@ func Init() { bus.AddHandler("search", searchHandler) jsonIndexCfg, _ := setting.Cfg.GetSection("dashboards.json") + + if jsonIndexCfg == nil { + log.Fatal("Config section missing: dashboards.json") + return + } + jsonIndexEnabled := jsonIndexCfg.Key("enabled").MustBool(false) if jsonIndexEnabled { diff --git a/pkg/search/handlers_test.go b/pkg/services/search/handlers_test.go similarity index 96% rename from pkg/search/handlers_test.go rename to pkg/services/search/handlers_test.go index dc9835caa44..bb355ec146f 100644 --- a/pkg/search/handlers_test.go +++ b/pkg/services/search/handlers_test.go @@ -11,7 +11,7 @@ import ( func TestSearch(t *testing.T) { Convey("Given search query", t, func() { - jsonDashIndex = NewJsonDashIndex("../../public/dashboards/") + jsonDashIndex = NewJsonDashIndex("../../../public/dashboards/") query := Query{Limit: 2000} bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error { diff --git a/pkg/search/json_index.go b/pkg/services/search/json_index.go similarity index 96% rename from pkg/search/json_index.go rename to pkg/services/search/json_index.go index a0fc02343e2..e70c662438d 100644 --- a/pkg/search/json_index.go +++ b/pkg/services/search/json_index.go @@ -51,13 +51,15 @@ func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) { return results, nil } + queryStr := strings.ToLower(query.Title) + for _, item := range index.items { if len(results) > query.Limit { break } // add results with matchig title filter - if strings.Contains(item.TitleLower, query.Title) { + if strings.Contains(item.TitleLower, queryStr) { results = append(results, &Hit{ Type: DashHitJson, Title: item.Dashboard.Title, diff --git a/pkg/search/json_index_test.go b/pkg/services/search/json_index_test.go similarity index 93% rename from pkg/search/json_index_test.go rename to pkg/services/search/json_index_test.go index afd584fffbd..145e1ac1e99 100644 --- a/pkg/search/json_index_test.go +++ b/pkg/services/search/json_index_test.go @@ -9,7 +9,7 @@ import ( func TestJsonDashIndex(t *testing.T) { Convey("Given the json dash index", t, func() { - index := NewJsonDashIndex("../../public/dashboards/") + index := NewJsonDashIndex("../../../public/dashboards/") Convey("Should be able to update index", func() { err := index.updateIndex() diff --git a/pkg/search/models.go b/pkg/services/search/models.go similarity index 100% rename from pkg/search/models.go rename to pkg/services/search/models.go diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 01eacb8436a..7fdaace316e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/search" + "github.com/grafana/grafana/pkg/services/search" ) func init() { diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go index 0bbb01ed6bd..f4611050a77 100644 --- a/pkg/services/sqlstore/dashboard_snapshot.go +++ b/pkg/services/sqlstore/dashboard_snapshot.go @@ -57,7 +57,7 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error { if err != nil { return err } else if has == false { - return m.ErrNotFound + return m.ErrDashboardSnapshotNotFound } query.Result = &snapshot diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 0ddd0db0df6..0d4eb111868 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -6,7 +6,7 @@ import ( . "github.com/smartystreets/goconvey/convey" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/search" + "github.com/grafana/grafana/pkg/services/search" ) func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 3cb4792ca82..6dd583a586b 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -38,7 +38,6 @@ const ( var ( // App settings. Env string = DEV - AppName string AppUrl string AppSubUrl string @@ -68,18 +67,18 @@ var ( EnforceDomain bool // Security settings. - SecretKey string - LogInRememberDays int - CookieUserName string - CookieRememberName string - DisableGravatar bool + SecretKey string + LogInRememberDays int + CookieUserName string + CookieRememberName string + DisableGravatar bool + EmailCodeValidMinutes int // User settings AllowUserSignUp bool AllowUserOrgCreate bool AutoAssignOrg bool AutoAssignOrgRole string - ViewerRoleMode string // Http auth AdminUser string @@ -95,6 +94,9 @@ var ( AuthProxyHeaderProperty string AuthProxyAutoSignUp bool + // Basic Auth + BasicAuthEnabled bool + // Session settings. SessionOptions session.Options @@ -117,7 +119,10 @@ var ( // LDAP LdapEnabled bool - LdapUrls []string + LdapHosts []string + + // SMTP email settings + Smtp SmtpSettings ) type CommandLineArgs struct { @@ -351,7 +356,6 @@ func NewConfigContext(args *CommandLineArgs) { setHomePath(args) loadConfiguration(args) - AppName = Cfg.Section("").Key("app_name").MustString("Grafana") Env = Cfg.Section("").Key("app_mode").MustString("development") server := Cfg.Section("server") @@ -388,7 +392,6 @@ func NewConfigContext(args *CommandLineArgs) { AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) - ViewerRoleMode = users.Key("viewer_role_mode").In("default", []string{"default", "strinct"}) // anonymous access AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false) @@ -402,6 +405,9 @@ func NewConfigContext(args *CommandLineArgs) { AuthProxyHeaderProperty = authProxy.Key("header_property").String() AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) + authBasic := Cfg.Section("auth.basic") + BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) + // PhantomJS rendering ImagesDir = filepath.Join(DataPath, "png") PhantomDir = filepath.Join(HomePath, "vendor/phantomjs") @@ -412,9 +418,10 @@ func NewConfigContext(args *CommandLineArgs) { ldapSec := Cfg.Section("auth.ldap") LdapEnabled = ldapSec.Key("enabled").MustBool(false) - LdapUrls = ldapSec.Key("urls").Strings(" ") + LdapHosts = ldapSec.Key("hosts").Strings(" ") readSessionConfig() + readSmtpSettings() } func readSessionConfig() { diff --git a/pkg/setting/setting_smtp.go b/pkg/setting/setting_smtp.go new file mode 100644 index 00000000000..e84b61634cc --- /dev/null +++ b/pkg/setting/setting_smtp.go @@ -0,0 +1,31 @@ +package setting + +type SmtpSettings struct { + Enabled bool + Host string + User string + Password string + CertFile string + KeyFile string + FromAddress string + SkipVerify bool + + SendWelcomeEmailOnSignUp bool + TemplatesPattern string +} + +func readSmtpSettings() { + sec := Cfg.Section("smtp") + Smtp.Enabled = sec.Key("enabled").MustBool(false) + Smtp.Host = sec.Key("host").String() + Smtp.User = sec.Key("user").String() + Smtp.Password = sec.Key("password").String() + Smtp.CertFile = sec.Key("cert_file").String() + Smtp.KeyFile = sec.Key("key_file").String() + Smtp.FromAddress = sec.Key("from_address").String() + Smtp.SkipVerify = sec.Key("skip_verify").MustBool(false) + + emails := Cfg.Section("emails") + Smtp.SendWelcomeEmailOnSignUp = emails.Key("welcome_email_on_sign_up").MustBool(false) + Smtp.TemplatesPattern = emails.Key("templates_pattern").MustString("emails/*.html") +} diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 73ccabd2dbc..00da9b4f416 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -15,7 +15,6 @@ func TestLoadingSettings(t *testing.T) { Convey("Given the default ini files", func() { NewConfigContext(&CommandLineArgs{HomePath: "../../"}) - So(AppName, ShouldEqual, "Grafana") So(AdminUser, ShouldEqual, "admin") }) diff --git a/pkg/util/encoding.go b/pkg/util/encoding.go index 27169133a42..e87da9d3d55 100644 --- a/pkg/util/encoding.go +++ b/pkg/util/encoding.go @@ -7,8 +7,10 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "errors" "fmt" "hash" + "strings" ) // source: https://github.com/gogits/gogs/blob/9ee80e3e5426821f03a4e99fad34418f5c736413/modules/base/tool.go#L58 @@ -80,3 +82,23 @@ func GetBasicAuthHeader(user string, password string) string { var userAndPass = user + ":" + password return "Basic " + base64.StdEncoding.EncodeToString([]byte(userAndPass)) } + +func DecodeBasicAuthHeader(header string) (string, string, error) { + var code string + parts := strings.SplitN(header, " ", 2) + if len(parts) == 2 && parts[0] == "Basic" { + code = parts[1] + } + + decoded, err := base64.StdEncoding.DecodeString(code) + if err != nil { + return "", "", err + } + + userAndPass := strings.SplitN(string(decoded), ":", 2) + if len(userAndPass) != 2 { + return "", "", errors.New("Invalid basic auth header") + } + + return userAndPass[0], userAndPass[1], nil +} diff --git a/pkg/util/encoding_test.go b/pkg/util/encoding_test.go index afe299f9f92..abcf5425826 100644 --- a/pkg/util/encoding_test.go +++ b/pkg/util/encoding_test.go @@ -13,4 +13,14 @@ func TestEncoding(t *testing.T) { So(result, ShouldEqual, "Basic Z3JhZmFuYToxMjM0") }) + + Convey("When decoding basic auth header", t, func() { + header := GetBasicAuthHeader("grafana", "1234") + username, password, err := DecodeBasicAuthHeader(header) + So(err, ShouldBeNil) + + So(username, ShouldEqual, "grafana") + So(password, ShouldEqual, "1234") + }) + } diff --git a/pkg/util/validation.go b/pkg/util/validation.go new file mode 100644 index 00000000000..dd7404e6de4 --- /dev/null +++ b/pkg/util/validation.go @@ -0,0 +1,18 @@ +package util + +import ( + "regexp" + "strings" +) + +const ( + emailRegexPattern string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" +) + +var ( + regexEmail = regexp.MustCompile(emailRegexPattern) +) + +func IsEmail(str string) bool { + return regexEmail.MatchString(strings.ToLower(str)) +} diff --git a/public/app/components/extend-jquery.js b/public/app/components/extend-jquery.js index 3e1f6b0c054..f44245103b5 100644 --- a/public/app/components/extend-jquery.js +++ b/public/app/components/extend-jquery.js @@ -24,14 +24,14 @@ function ($, angular, _) { $tooltip.appendTo(document.body); if (opts.compile) { - angular.element(document).injector().invoke(function($compile, $rootScope) { + angular.element(document).injector().invoke(["$compile", "$rootScope", function($compile, $rootScope) { var tmpScope = $rootScope.$new(true); _.extend(tmpScope, opts.scopeData); $compile($tooltip)(tmpScope); tmpScope.$digest(); - //tmpScope.$destroy(); - }); + tmpScope.$destroy(); + }]); } width = $tooltip.outerWidth(true); diff --git a/public/app/components/kbn.js b/public/app/components/kbn.js index c9d2900f53b..d817ada2ebe 100644 --- a/public/app/components/kbn.js +++ b/public/app/components/kbn.js @@ -399,6 +399,8 @@ function($, _, moment) { kbn.valueFormats.celsius = function(value, decimals) { return kbn.toFixed(value, decimals) + ' °C'; }; kbn.valueFormats.farenheit = function(value, decimals) { return kbn.toFixed(value, decimals) + ' °F'; }; kbn.valueFormats.humidity = function(value, decimals) { return kbn.toFixed(value, decimals) + ' %H'; }; + kbn.valueFormats.pressurembar = function(value, decimals) { return kbn.toFixed(value, decimals) + ' mbar'; }; + kbn.valueFormats.pressurehpa = function(value, decimals) { return kbn.toFixed(value, decimals) + ' hPa'; }; kbn.valueFormats.ppm = function(value, decimals) { return kbn.toFixed(value, decimals) + ' ppm'; }; kbn.valueFormats.velocityms = function(value, decimals) { return kbn.toFixed(value, decimals) + ' m/s'; }; kbn.valueFormats.velocitykmh = function(value, decimals) { return kbn.toFixed(value, decimals) + ' km/h'; }; @@ -406,6 +408,7 @@ function($, _, moment) { kbn.valueFormats.velocityknot = function(value, decimals) { return kbn.toFixed(value, decimals) + ' kn'; }; kbn.roundValue = function (num, decimals) { + if (num === null) { return null; } var n = Math.pow(10, decimals); return Math.round((n * num).toFixed(decimals)) / n; }; @@ -540,6 +543,7 @@ function($, _, moment) { {text: 'short', value: 'short'}, {text: 'percent', value: 'percent'}, {text: 'ppm', value: 'ppm'}, + {text: 'dB', value: 'dB'}, ] }, { @@ -589,6 +593,8 @@ function($, _, moment) { {text: 'Celcius (°C)', value: 'celsius' }, {text: 'Farenheit (°F)', value: 'farenheit'}, {text: 'Humidity (%H)', value: 'humidity' }, + {text: 'Pressure (mbar)', value: 'pressurembar' }, + {text: 'Pressure (hPa)', value: 'pressurehpa' }, ] }, { diff --git a/public/app/components/panelmeta.js b/public/app/components/panelmeta.js index 013919c174a..7eee8fa970f 100644 --- a/public/app/components/panelmeta.js +++ b/public/app/components/panelmeta.js @@ -13,12 +13,12 @@ function () { this.extendedMenu = []; if (options.fullscreen) { - this.addMenuItem('view', 'icon-eye-open', 'toggleFullscreen(false); dismiss();'); + this.addMenuItem('View', 'icon-eye-open', 'toggleFullscreen(false); dismiss();'); } - this.addMenuItem('edit', 'icon-cog', 'editPanel(); dismiss();', 'Editor'); - this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()', 'Editor'); - this.addMenuItem('share', 'icon-share', 'sharePanel(); dismiss();'); + this.addMenuItem('Edit', 'icon-cog', 'editPanel(); dismiss();', 'Editor'); + this.addMenuItem('Duplicate', 'icon-copy', 'duplicatePanel()', 'Editor'); + this.addMenuItem('Share', 'icon-share', 'sharePanel(); dismiss();'); this.addEditorTab('General', 'app/partials/panelgeneral.html'); diff --git a/public/app/components/require.config.js b/public/app/components/require.config.js index bbe9674aecf..12a72dd42e3 100644 --- a/public/app/components/require.config.js +++ b/public/app/components/require.config.js @@ -1,4 +1,3 @@ - require.config({ urlArgs: 'bust=' + (new Date().getTime()), baseUrl: 'public/app', @@ -9,19 +8,18 @@ require.config({ kbn: 'components/kbn', store: 'components/store', - css: '../vendor/require/css', - text: '../vendor/require/text', + text: '../vendor/requirejs-text/text', moment: '../vendor/moment', filesaver: '../vendor/filesaver', ZeroClipboard: '../vendor/ZeroClipboard', angular: '../vendor/angular/angular', - 'angular-route': '../vendor/angular/angular-route', - 'angular-sanitize': '../vendor/angular/angular-sanitize', - 'angular-dragdrop': '../vendor/angular/angular-dragdrop', - 'angular-strap': '../vendor/angular/angular-strap', - timepicker: '../vendor/angular/timepicker', - datepicker: '../vendor/angular/datepicker', - bindonce: '../vendor/angular/bindonce', + 'angular-route': '../vendor/angular-route/angular-route', + 'angular-sanitize': '../vendor/angular-sanitize/angular-sanitize', + 'angular-dragdrop': '../vendor/angular-native-dragdrop/draganddrop', + 'angular-strap': '../vendor/angular-other/angular-strap', + timepicker: '../vendor/angular-other/timepicker', + datepicker: '../vendor/angular-other/datepicker', + bindonce: '../vendor/angular-bindonce/bindonce', crypto: '../vendor/crypto.min', spectrum: '../vendor/spectrum', @@ -29,19 +27,19 @@ require.config({ 'lodash-src': '../vendor/lodash', bootstrap: '../vendor/bootstrap/bootstrap', - jquery: '../vendor/jquery/jquery-2.1.3', + jquery: '../vendor/jquery/dist/jquery', 'extend-jquery': 'components/extend-jquery', - 'jquery.flot': '../vendor/jquery/jquery.flot', - 'jquery.flot.pie': '../vendor/jquery/jquery.flot.pie', - 'jquery.flot.events': '../vendor/jquery/jquery.flot.events', - 'jquery.flot.selection': '../vendor/jquery/jquery.flot.selection', - 'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack', - 'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent', - 'jquery.flot.time': '../vendor/jquery/jquery.flot.time', - 'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair', - 'jquery.flot.fillbelow': '../vendor/jquery/jquery.flot.fillbelow', + 'jquery.flot': '../vendor/flot/jquery.flot', + 'jquery.flot.pie': '../vendor/flot/jquery.flot.pie', + 'jquery.flot.events': '../vendor/flot/jquery.flot.events', + 'jquery.flot.selection': '../vendor/flot/jquery.flot.selection', + 'jquery.flot.stack': '../vendor/flot/jquery.flot.stack', + 'jquery.flot.stackpercent':'../vendor/flot/jquery.flot.stackpercent', + 'jquery.flot.time': '../vendor/flot/jquery.flot.time', + 'jquery.flot.crosshair': '../vendor/flot/jquery.flot.crosshair', + 'jquery.flot.fillbelow': '../vendor/flot/jquery.flot.fillbelow', modernizr: '../vendor/modernizr-2.6.1', @@ -101,5 +99,4 @@ require.config({ 'bootstrap-tagsinput': ['jquery'], }, - waitSeconds: 60, }); diff --git a/public/app/components/timeSeries.js b/public/app/components/timeSeries.js index 679777bfb92..3c41378947e 100644 --- a/public/app/components/timeSeries.js +++ b/public/app/components/timeSeries.js @@ -54,6 +54,7 @@ function (_, kbn) { if (override.zindex !== void 0) { this.zindex = override.zindex; } if (override.fillBelowTo !== void 0) { this.fillBelowTo = override.fillBelowTo; } if (override.color !== void 0) { this.color = override.color; } + if (override.transform !== void 0) { this.transform = override.transform; } if (override.yaxis !== void 0) { this.yaxis = override.yaxis; diff --git a/public/app/controllers/all.js b/public/app/controllers/all.js index f735963e886..99b9a496484 100644 --- a/public/app/controllers/all.js +++ b/public/app/controllers/all.js @@ -6,6 +6,7 @@ define([ './inspectCtrl', './jsonEditorCtrl', './loginCtrl', + './resetPasswordCtrl', './sidemenuCtrl', './errorCtrl', ], function () {}); diff --git a/public/app/controllers/loginCtrl.js b/public/app/controllers/loginCtrl.js index 767f788cea3..40e8009b399 100644 --- a/public/app/controllers/loginCtrl.js +++ b/public/app/controllers/loginCtrl.js @@ -21,13 +21,10 @@ function (angular, config) { $scope.disableUserSignUp = config.disableUserSignUp; $scope.loginMode = true; - $scope.submitBtnClass = 'btn-inverse'; $scope.submitBtnText = 'Log in'; - $scope.strengthClass = ''; $scope.init = function() { $scope.$watch("loginMode", $scope.loginModeChanged); - $scope.passwordChanged(); var params = $location.search(); if (params.failedMsg) { @@ -56,27 +53,6 @@ function (angular, config) { $scope.submitBtnText = newValue ? 'Log in' : 'Sign up'; }; - $scope.passwordChanged = function(newValue) { - if (!newValue) { - $scope.strengthText = ""; - $scope.strengthClass = "hidden"; - return; - } - if (newValue.length < 4) { - $scope.strengthText = "strength: weak sauce."; - $scope.strengthClass = "password-strength-bad"; - return; - } - if (newValue.length <= 8) { - $scope.strengthText = "strength: you can do better."; - $scope.strengthClass = "password-strength-ok"; - return; - } - - $scope.strengthText = "strength: strong like a bull."; - $scope.strengthClass = "password-strength-good"; - }; - $scope.signUp = function() { if (!$scope.loginForm.$valid) { return; diff --git a/public/app/controllers/resetPasswordCtrl.js b/public/app/controllers/resetPasswordCtrl.js new file mode 100644 index 00000000000..ed693f0d45a --- /dev/null +++ b/public/app/controllers/resetPasswordCtrl.js @@ -0,0 +1,45 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('ResetPasswordCtrl', function($scope, contextSrv, backendSrv, $location) { + + contextSrv.sidemenu = false; + $scope.formModel = {}; + $scope.mode = 'send'; + + var params = $location.search(); + if (params.code) { + $scope.mode = 'reset'; + $scope.formModel.code = params.code; + } + + $scope.sendResetEmail = function() { + if (!$scope.sendResetForm.$valid) { + return; + } + backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(function() { + $scope.mode = 'email-sent'; + }); + }; + + $scope.submitReset = function() { + if (!$scope.resetForm.$valid) { return; } + + if ($scope.formModel.newPassword !== $scope.formModel.confirmPassword) { + $scope.appEvent('alert-warning', ['New passwords do not match', '']); + return; + } + + backendSrv.post('/api/user/password/reset', $scope.formModel).then(function() { + $location.path('login'); + }); + }; + + }); + +}); diff --git a/public/app/controllers/sidemenuCtrl.js b/public/app/controllers/sidemenuCtrl.js index f5a9197caac..b7ba32f0d35 100644 --- a/public/app/controllers/sidemenuCtrl.js +++ b/public/app/controllers/sidemenuCtrl.js @@ -55,7 +55,7 @@ function (angular, _, $, config) { backendSrv.get('/api/user/orgs').then(function(orgs) { _.each(orgs, function(org) { - if (org.isUsing) { + if (org.orgId === contextSrv.user.orgId) { return; } @@ -68,11 +68,13 @@ function (angular, _, $, config) { }); }); - $scope.orgMenu.push({ - text: "New Organization", - icon: "fa fa-fw fa-plus", - href: $scope.getUrl('/org/new') - }); + if (config.allowOrgCreate) { + $scope.orgMenu.push({ + text: "New Organization", + icon: "fa fa-fw fa-plus", + href: $scope.getUrl('/org/new') + }); + } }); }; diff --git a/public/app/directives/all.js b/public/app/directives/all.js index b92bc59ca41..13a8accffbd 100644 --- a/public/app/directives/all.js +++ b/public/app/directives/all.js @@ -11,11 +11,12 @@ define([ './spectrumPicker', './tags', './bodyClass', - './variableValueSelect', + './valueSelectDropdown', './metric.segment', './grafanaVersionCheck', './dropdown.typeahead', './topnav', './giveFocus', './annotationTooltip', + './passwordStrenght', ], function () {}); diff --git a/public/app/directives/metric.segment.js b/public/app/directives/metric.segment.js index 4f5677ca3ed..4202cfdc332 100644 --- a/public/app/directives/metric.segment.js +++ b/public/app/directives/metric.segment.js @@ -103,8 +103,19 @@ function (angular, app, _, $) { return value; }; + $scope.matcher = function(item) { + var str = this.query; + if (str[0] === '/') { str = str.substring(1); } + if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); } + try { + return item.toLowerCase().match(str); + } catch(e) { + return false; + } + }; + $input.attr('data-provide', 'typeahead'); - $input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater }); + $input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater, matcher: $scope.matcher }); var typeahead = $input.data('typeahead'); typeahead.lookup = function () { diff --git a/public/app/directives/passwordStrenght.js b/public/app/directives/passwordStrenght.js new file mode 100644 index 00000000000..f75a8fe8854 --- /dev/null +++ b/public/app/directives/passwordStrenght.js @@ -0,0 +1,47 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('passwordStrength', function() { + var template = '
    ' + + '{{strengthText}}' + + '
    '; + return { + template: template, + scope: { + password: "=", + }, + link: function($scope) { + + $scope.strengthClass = ''; + + function passwordChanged(newValue) { + if (!newValue) { + $scope.strengthText = ""; + $scope.strengthClass = "hidden"; + return; + } + if (newValue.length < 4) { + $scope.strengthText = "strength: weak sauce."; + $scope.strengthClass = "password-strength-bad"; + return; + } + if (newValue.length <= 8) { + $scope.strengthText = "strength: you can do better."; + $scope.strengthClass = "password-strength-ok"; + return; + } + + $scope.strengthText = "strength: strong like a bull."; + $scope.strengthClass = "password-strength-good"; + } + + $scope.$watch("password", passwordChanged); + } + }; + }); +}); diff --git a/public/app/directives/variableValueSelect.js b/public/app/directives/valueSelectDropdown.js similarity index 83% rename from public/app/directives/variableValueSelect.js rename to public/app/directives/valueSelectDropdown.js index ae08ad55ee4..65b45135fd5 100644 --- a/public/app/directives/variableValueSelect.js +++ b/public/app/directives/valueSelectDropdown.js @@ -9,59 +9,53 @@ function (angular, app, _) { angular .module('grafana.controllers') - .controller('SelectDropdownCtrl', function($q) { + .controller('ValueSelectDropdownCtrl', function($q) { var vm = this; vm.show = function() { vm.oldVariableText = vm.variable.current.text; vm.highlightIndex = -1; - var currentValues = vm.variable.current.value; - if (_.isString(currentValues)) { - currentValues = [currentValues]; - } - - vm.options = _.map(vm.variable.options, function(option) { - if (_.indexOf(currentValues, option.value) >= 0) { option.selected = true; } - return option; - }); - - _.sortBy(vm.options, 'text'); - + vm.options = vm.variable.options; vm.selectedValues = _.filter(vm.options, {selected: true}); vm.tags = _.map(vm.variable.tags, function(value) { - return { text: value, selected: false }; + var tag = { text: value, selected: false }; + _.each(vm.variable.current.tags, function(tagObj) { + if (tagObj.text === value) { + tag = tagObj; + } + }); + return tag; }); - vm.search = {query: '', options: vm.options}; + vm.search = { + query: '', + options: vm.options.slice(0, Math.min(vm.options.length, 1000)) + }; + vm.dropdownVisible = true; }; vm.updateLinkText = function() { var current = vm.variable.current; - var currentValues = current.value; - if (_.isArray(currentValues) && current.tags.length) { + if (current.tags && current.tags.length) { // filer out values that are in selected tags - currentValues = _.filter(currentValues, function(test) { - for (var i = 0; i < current.tags.length; i++) { - if (_.indexOf(current.tags[i].values, test) !== -1) { + var selectedAndNotInTag = _.filter(vm.variable.options, function(option) { + if (!option.selected) { return false; } + for (var i = 0; i < current.tags.length; i++) { + var tag = current.tags[i]; + if (_.indexOf(tag.values, option.value) !== -1) { return false; } } return true; }); + // convert values to text - var currentTexts = _.map(currentValues, function(value) { - for (var i = 0; i < vm.variable.options.length; i++) { - var option = vm.variable.options[i]; - if (option.value === value) { - return option.text; - } - } - return value; - }); + var currentTexts = _.pluck(selectedAndNotInTag, 'text'); + // join texts vm.linkText = currentTexts.join(' + '); if (vm.linkText.length > 0) { @@ -214,6 +208,8 @@ function (angular, app, _) { vm.search.options = _.filter(vm.options, function(option) { return option.text.toLowerCase().indexOf(vm.search.query.toLowerCase()) !== -1; }); + + vm.search.options = vm.search.options.slice(0, Math.min(vm.search.options.length, 1000)); }; vm.init = function() { @@ -225,12 +221,12 @@ function (angular, app, _) { angular .module('grafana.directives') - .directive('variableValueSelect', function($compile, $window, $timeout) { + .directive('valueSelectDropdown', function($compile, $window, $timeout, $rootScope) { return { scope: { variable: "=", onUpdated: "&", getValuesForTag: "&" }, - templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', - controller: 'SelectDropdownCtrl', + templateUrl: 'app/partials/valueSelectDropdown.html', + controller: 'ValueSelectDropdownCtrl', controllerAs: 'vm', bindToController: true, link: function(scope, elem) { @@ -270,6 +266,14 @@ function (angular, app, _) { } }); + var cleanUp = $rootScope.$on('template-variable-value-updated', function() { + scope.vm.updateLinkText(); + }); + + scope.$on("$destroy", function() { + cleanUp(); + }); + scope.vm.init(); }, }; diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index ffead046211..f25fea26afe 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -84,6 +84,7 @@ function (angular, $, config) { }; $scope.broadcastRefresh = function() { + $rootScope.performance.panelsRendered = 0; $rootScope.$broadcast('refresh'); }; diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index dcb4e31340c..bb58e2dbc06 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -91,7 +91,7 @@ function (angular, _) { // returns a new panel clone or reuses a clone from previous iteration this.repeatRow = function(row) { var variables = this.dashboard.templating.list; - var variable = _.findWhere(variables, {name: row.repeat.replace('$', '')}); + var variable = _.findWhere(variables, {name: row.repeat}); if (!variable) { return; } @@ -105,6 +105,8 @@ function (angular, _) { _.each(selected, function(option, index) { copy = self.getRowClone(row, index); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; for (i = 0; i < copy.panels.length; i++) { panel = copy.panels[i]; @@ -162,6 +164,7 @@ function (angular, _) { _.each(selected, function(option, index) { var copy = self.getPanelClone(panel, row, index); + copy.span = Math.max(12 / selected.length, panel.minSpan); copy.scopedVars = copy.scopedVars || {}; copy.scopedVars[variable.name] = option; }); diff --git a/public/app/features/dashboard/playlistCtrl.js b/public/app/features/dashboard/playlistCtrl.js index 5320a0b808e..b5d04374e9a 100644 --- a/public/app/features/dashboard/playlistCtrl.js +++ b/public/app/features/dashboard/playlistCtrl.js @@ -25,14 +25,14 @@ function (angular, _, config) { } backendSrv.search(query).then(function(results) { - $scope.searchHits = results.dashboards; + $scope.searchHits = results; $scope.filterHits(); }); }; $scope.filterHits = function() { $scope.filteredHits = _.reject($scope.searchHits, function(dash) { - return _.findWhere($scope.playlist, {slug: dash.slug}); + return _.findWhere($scope.playlist, {uri: dash.uri}); }); }; diff --git a/public/app/features/dashboard/playlistSrv.js b/public/app/features/dashboard/playlistSrv.js index 0711cb7c453..9997581fbc3 100644 --- a/public/app/features/dashboard/playlistSrv.js +++ b/public/app/features/dashboard/playlistSrv.js @@ -18,7 +18,7 @@ function (angular, _, kbn) { angular.element(window).unbind('resize'); var dash = self.dashboards[self.index % self.dashboards.length]; - $location.url('dashboard/db/' + dash.slug); + $location.url('dashboard/' + dash.uri); self.index++; self.cancelPromise = $timeout(self.next, self.interval); diff --git a/public/app/features/dashboard/rowCtrl.js b/public/app/features/dashboard/rowCtrl.js index 1f839bd206a..c63017365bb 100644 --- a/public/app/features/dashboard/rowCtrl.js +++ b/public/app/features/dashboard/rowCtrl.js @@ -22,7 +22,6 @@ function (angular, app, _, config) { $scope.init = function() { $scope.editor = {index: 0}; - $scope.reset_panel(); }; $scope.togglePanelMenu = function(posX) { @@ -64,8 +63,18 @@ function (angular, app, _, config) { }; $scope.add_panel_default = function(type) { - $scope.reset_panel(type); - $scope.add_panel($scope.panel); + var defaultSpan = 12; + var _as = 12 - $scope.dashboard.rowSpan($scope.row); + + var panel = { + title: config.new_panel_title, + error: false, + span: _as < defaultSpan && _as > 0 ? _as : defaultSpan, + editable: true, + type: type + }; + + $scope.add_panel(panel); $timeout(function() { $scope.$broadcast('render'); @@ -105,31 +114,6 @@ function (angular, app, _, config) { }); }; - $scope.reset_panel = function(type) { - var defaultSpan = 12; - var _as = 12 - $scope.dashboard.rowSpan($scope.row); - - $scope.panel = { - title: config.new_panel_title, - error: false, - span: _as < defaultSpan && _as > 0 ? _as : defaultSpan, - editable: true, - type: type - }; - - function fixRowHeight(height) { - if (!height) { - return '200px'; - } - if (!_.isString(height)) { - return height + 'px'; - } - return height; - } - - $scope.row.height = fixRowHeight($scope.row.height); - }; - $scope.init(); }); diff --git a/public/app/features/dashboard/submenuCtrl.js b/public/app/features/dashboard/submenuCtrl.js index b8e609c061e..b1d0dc3ae32 100644 --- a/public/app/features/dashboard/submenuCtrl.js +++ b/public/app/features/dashboard/submenuCtrl.js @@ -1,18 +1,12 @@ define([ 'angular', - 'lodash' ], -function (angular, _) { +function (angular) { 'use strict'; var module = angular.module('grafana.controllers'); module.controller('SubmenuCtrl', function($scope, $q, $rootScope, templateValuesSrv, dynamicDashboardSrv) { - var _d = { - enable: true - }; - - _.defaults($scope.pulldown,_d); $scope.init = function() { $scope.panel = $scope.pulldown; @@ -33,6 +27,7 @@ function (angular, _) { $scope.variableUpdated = function(variable) { templateValuesSrv.variableUpdated(variable).then(function() { dynamicDashboardSrv.update($scope.dashboard); + $rootScope.$emit('template-variable-value-updated'); $rootScope.$broadcast('refresh'); }); }; diff --git a/public/app/features/dashlinks/module.js b/public/app/features/dashlinks/module.js index 7be8ffbd87b..9fc1bc4d4b5 100644 --- a/public/app/features/dashlinks/module.js +++ b/public/app/features/dashlinks/module.js @@ -105,7 +105,7 @@ function (angular, _) { }]); } - return $scope.searchDashboards(linkDef); + return $scope.searchDashboards(linkDef, 7); } if (linkDef.type === 'link') { @@ -131,8 +131,8 @@ function (angular, _) { }); } - $scope.searchDashboards = function(link) { - return backendSrv.search({tag: link.tags}).then(function(results) { + $scope.searchDashboards = function(link, limit) { + return backendSrv.search({tag: link.tags, limit: limit}).then(function(results) { return _.reduce(results, function(memo, dash) { // do not add current dashboard if (dash.id !== currentDashId) { @@ -150,7 +150,7 @@ function (angular, _) { }; $scope.fillDropdown = function(link) { - $scope.searchDashboards(link).then(function(results) { + $scope.searchDashboards(link, 100).then(function(results) { _.each(results, function(hit) { hit.url = linkSrv.getLinkUrl(hit); }); diff --git a/public/app/features/org/datasourceEditCtrl.js b/public/app/features/org/datasourceEditCtrl.js index f2af6325a20..9bb49edff42 100644 --- a/public/app/features/org/datasourceEditCtrl.js +++ b/public/app/features/org/datasourceEditCtrl.js @@ -58,7 +58,7 @@ function (angular, config) { }; $scope.updateFrontendSettings = function() { - backendSrv.get('/api/frontend/settings').then(function(settings) { + return backendSrv.get('/api/frontend/settings').then(function(settings) { config.datasources = settings.datasources; config.defaultDatasource = settings.defaultDatasource; datasourceSrv.init(); @@ -101,12 +101,13 @@ function (angular, config) { if ($scope.current.id) { return backendSrv.put('/api/datasources/' + $scope.current.id, $scope.current).then(function() { - $scope.updateFrontendSettings(); - if (test) { - $scope.testDatasource(); - } else { - $location.path('datasources'); - } + $scope.updateFrontendSettings().then(function() { + if (test) { + $scope.testDatasource(); + } else { + $location.path('datasources'); + } + }); }); } else { return backendSrv.post('/api/datasources', $scope.current).then(function(result) { diff --git a/public/app/features/org/newOrgCtrl.js b/public/app/features/org/newOrgCtrl.js index 9d81e226dce..220192a46ac 100644 --- a/public/app/features/org/newOrgCtrl.js +++ b/public/app/features/org/newOrgCtrl.js @@ -1,7 +1,8 @@ define([ 'angular', + 'config', ], -function (angular) { +function (angular, config) { 'use strict'; var module = angular.module('grafana.controllers'); @@ -11,7 +12,11 @@ function (angular) { $scope.newOrg = {name: ''}; $scope.createOrg = function() { - backendSrv.post('/api/orgs/', $scope.newOrg).then($scope.getUserOrgs); + backendSrv.post('/api/orgs/', $scope.newOrg).then(function(result) { + backendSrv.post('/api/user/using/' + result.orgId).then(function() { + window.location.href = config.appSubUrl + '/org'; + }); + }); }; }); diff --git a/public/app/features/panel/panelMenu.js b/public/app/features/panel/panelMenu.js index 5a701084ff3..c7711d99925 100644 --- a/public/app/features/panel/panelMenu.js +++ b/public/app/features/panel/panelMenu.js @@ -155,6 +155,9 @@ function (angular, $, _) { if (panelLeftPos + menuLeftPos < 0) { menuLeftPos = 0; } + if ($scope.fullscreen) { + menuHeight = -(menuHeight/2); + } $menu.css({'left': menuLeftPos, top: -menuHeight}); }); diff --git a/public/app/features/panel/panelSrv.js b/public/app/features/panel/panelSrv.js index b863518ff72..037125b4aad 100644 --- a/public/app/features/panel/panelSrv.js +++ b/public/app/features/panel/panelSrv.js @@ -86,6 +86,10 @@ function (angular, _, config) { return datasourceSrv.get($scope.panel.datasource); }; + $scope.panelRenderingComplete = function() { + $rootScope.performance.panelsRendered++; + }; + $scope.get_data = function() { if ($scope.otherPanelInFullscreenMode()) { return; } diff --git a/public/app/features/panellinks/linkSrv.js b/public/app/features/panellinks/linkSrv.js index 2f943b13f08..c11cdb40b52 100644 --- a/public/app/features/panellinks/linkSrv.js +++ b/public/app/features/panellinks/linkSrv.js @@ -62,21 +62,34 @@ function (angular, kbn, _) { this.getPanelLinkAnchorInfo = function(link) { var info = {}; if (link.type === 'absolute') { - info.target = '_blank'; + info.target = link.targetBlank ? '_blank' : ''; info.href = templateSrv.replace(link.url || ''); info.title = templateSrv.replace(link.title || ''); info.href += '?'; } + else if (link.dashUri) { + info.href = 'dashboard/' + link.dashUri + '?'; + info.title = templateSrv.replace(link.title || ''); + } else { info.title = templateSrv.replace(link.title || ''); var slug = kbn.slugifyForUrl(link.dashboard || ''); info.href = 'dashboard/db/' + slug + '?'; } - var range = timeSrv.timeRangeForUrl(); - info.href += 'from=' + range.from; - info.href += '&to=' + range.to; + var params = {}; + if (link.keepTime) { + var range = timeSrv.timeRangeForUrl(); + params['from'] = range.from; + params['to'] = range.to; + } + + if (link.includeVars) { + templateSrv.fillVariableValuesForUrl(params); + } + + info.href = this.addParamsToUrl(info.href, params); if (link.params) { info.href += "&" + templateSrv.replace(link.params); } diff --git a/public/app/features/panellinks/module.html b/public/app/features/panellinks/module.html index 7b3020e587e..99f93d21eda 100644 --- a/public/app/features/panellinks/module.html +++ b/public/app/features/panellinks/module.html @@ -2,49 +2,73 @@
    Drilldown / detail linkThese links appear in the dropdown menu in the panel menu.
    -
    -
    -
      +
      +
      +
      • + + +
      • +
      • +
      -
    • Link title
    • -
    • - +
        +
      • +
      • Type
      • - +
      • -
      • Dashboard
      • +
      • Dashboard
      • - +
      • -
      • Url
      • +
      • Url
      • - + +
      • + +
      +
      +
    • + +
      +
        +
      • + +
      • +
      • Title
      • +
      • + +
      • +
      • + Url params +
      • +
      • +
      -
      @@ -47,7 +47,7 @@ Limit number to
    • - +
    diff --git a/public/app/panels/dashlist/module.js b/public/app/panels/dashlist/module.js index 9b4e1ebc045..3e7c8c5587c 100644 --- a/public/app/panels/dashlist/module.js +++ b/public/app/panels/dashlist/module.js @@ -21,7 +21,7 @@ function (angular, app, _, config, PanelMeta) { module.controller('DashListPanelCtrl', function($scope, panelSrv, backendSrv) { $scope.panelMeta = new PanelMeta({ - panelName: 'Dash list', + panelName: 'Dashboard list', editIcon: "fa fa-star", fullscreen: true, }); @@ -66,6 +66,7 @@ function (angular, app, _, config, PanelMeta) { return backendSrv.search(params).then(function(result) { $scope.dashList = result; + $scope.panelRenderingComplete(); }); }; diff --git a/public/app/panels/graph/graph.js b/public/app/panels/graph/graph.js index c6ca8ec7227..0f6ae7baeeb 100755 --- a/public/app/panels/graph/graph.js +++ b/public/app/panels/graph/graph.js @@ -247,22 +247,26 @@ function (angular, $, kbn, moment, _, GraphTooltip) { sortedSeries = _.sortBy(data, function(series) { return series.zindex; }); - function callPlot() { + function callPlot(incrementRenderCounter) { try { $.plot(elem, sortedSeries, options); } catch (e) { console.log('flotcharts error', e); } + + if (incrementRenderCounter) { + scope.panelRenderingComplete(); + } } if (shouldDelayDraw(panel)) { // temp fix for legends on the side, need to render twice to get dimensions right - callPlot(); - setTimeout(callPlot, 50); + callPlot(false); + setTimeout(function() { callPlot(true); }, 50); legendSideLastValue = panel.legend.rightSide; } else { - callPlot(); + callPlot(true); } } diff --git a/public/app/panels/graph/seriesOverridesCtrl.js b/public/app/panels/graph/seriesOverridesCtrl.js index 81cb41a8572..8cbf7dff03a 100644 --- a/public/app/panels/graph/seriesOverridesCtrl.js +++ b/public/app/panels/graph/seriesOverridesCtrl.js @@ -99,10 +99,11 @@ define([ $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); $scope.addOverrideOption('Points', 'points', [true, false]); $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]); - $scope.addOverrideOption('Stack', 'stack', [true, false, 2, 3, 4, 5]); + $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']); $scope.addOverrideOption('Color', 'color', ['change']); $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]); $scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]); + $scope.addOverrideOption('Transform', 'transform', ['negative-Y']); $scope.updateCurrentOverrides(); }); diff --git a/public/app/panels/singlestat/module.js b/public/app/panels/singlestat/module.js index 24855634ba6..b38912605bb 100644 --- a/public/app/panels/singlestat/module.js +++ b/public/app/panels/singlestat/module.js @@ -186,14 +186,21 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) { data.flotpairs = []; if ($scope.series && $scope.series.length > 0) { - data.value = $scope.series[0].stats[$scope.panel.valueName]; - data.flotpairs = $scope.series[0].flotpairs; - } + var lastValue = _.last($scope.series[0].datapoints)[0]; + if (_.isString(lastValue)) { + data.value = 0; + data.valueFormated = lastValue; + data.valueRounded = 0; + } else { + data.value = $scope.series[0].stats[$scope.panel.valueName]; + data.flotpairs = $scope.series[0].flotpairs; - var decimalInfo = $scope.getDecimalsForValue(data.value); - var formatFunc = kbn.valueFormats[$scope.panel.format]; - data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); - data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); + var decimalInfo = $scope.getDecimalsForValue(data.value); + var formatFunc = kbn.valueFormats[$scope.panel.format]; + data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); + data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); + } + } // check value to text mappings for(var i = 0; i < $scope.panel.valueMaps.length; i++) { diff --git a/public/app/panels/singlestat/singleStatPanel.js b/public/app/panels/singlestat/singleStatPanel.js index 27a0167f05b..2463ca1e38e 100644 --- a/public/app/panels/singlestat/singleStatPanel.js +++ b/public/app/panels/singlestat/singleStatPanel.js @@ -20,6 +20,7 @@ function (angular, app, _, $) { scope.$on('render', function() { render(); + scope.panelRenderingComplete(); }); function setElementHeight() { @@ -181,9 +182,13 @@ function (angular, app, _, $) { elem.click(function() { if (panel.links.length === 0) { return; } - - var linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0]); - if (linkInfo.href[0] === '#') { linkInfo.href = linkInfo.href.substring(1); } + var link = panel.links[0]; + var linkInfo = linkSrv.getPanelLinkAnchorInfo(link); + if (panel.links[0].targetBlank) { + var redirectWindow = window.open(linkInfo.href, '_blank'); + redirectWindow.location; + return; + } if (linkInfo.href.indexOf('http') === 0) { window.location.href = linkInfo.href; diff --git a/public/app/panels/text/module.js b/public/app/panels/text/module.js index c5e82cd4199..436a9982b41 100644 --- a/public/app/panels/text/module.js +++ b/public/app/panels/text/module.js @@ -61,6 +61,7 @@ function (angular, app, _, require, PanelMeta) { else if ($scope.panel.mode === 'text') { $scope.renderText($scope.panel.content); } + $scope.panelRenderingComplete(); }; $scope.renderText = function(content) { diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index c9dca5c5f72..57c9cb74521 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -21,7 +21,7 @@
    -
    +
    -
    +
    diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 437588dd652..a5894866d5a 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -17,61 +17,61 @@
    - diff --git a/public/app/partials/panelgeneral.html b/public/app/partials/panelgeneral.html index 02db54cd6c6..0d7cbb2c939 100644 --- a/public/app/partials/panelgeneral.html +++ b/public/app/partials/panelgeneral.html @@ -42,6 +42,14 @@ +
  • + Min span +
  • +
  • + +
  • diff --git a/public/app/partials/reset_password.html b/public/app/partials/reset_password.html new file mode 100644 index 00000000000..2b1cee241ef --- /dev/null +++ b/public/app/partials/reset_password.html @@ -0,0 +1,90 @@ +
    + + +
    + diff --git a/public/app/partials/sidemenu.html b/public/app/partials/sidemenu.html index be2311273cd..f73ef658fba 100644 --- a/public/app/partials/sidemenu.html +++ b/public/app/partials/sidemenu.html @@ -36,7 +36,7 @@ diff --git a/public/app/features/dashboard/partials/variableValueSelect.html b/public/app/partials/valueSelectDropdown.html similarity index 81% rename from public/app/features/dashboard/partials/variableValueSelect.html rename to public/app/partials/valueSelectDropdown.html index 2c291448573..a75b7e8a6a9 100644 --- a/public/app/features/dashboard/partials/variableValueSelect.html +++ b/public/app/partials/valueSelectDropdown.html @@ -15,11 +15,11 @@
    - + Selected ({{vm.selectedValues.length}}) - + {{option.text}} diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 957d8b7ffe7..9568b66ad74 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -264,7 +264,7 @@ function (angular, _, config, kbn, moment) { var query = { query: { query_string: { query: queryString } }, facets: { tags: { terms: { field: "tags", order: "term", size: 50 } } }, - size: this.searchMaxResults, + size: 10000, sort: ["_uid"], }; diff --git a/public/app/plugins/datasource/graphite/lexer.js b/public/app/plugins/datasource/graphite/lexer.js index 7306737d96e..457100b163e 100644 --- a/public/app/plugins/datasource/graphite/lexer.js +++ b/public/app/plugins/datasource/graphite/lexer.js @@ -119,6 +119,8 @@ define([ identifierStartTable[i] = i >= 48 && i <= 57 || // 0-9 i === 36 || // $ + i === 126 || // ~ + i === 124 || // | i >= 65 && i <= 90 || // A-Z i === 95 || // _ i === 45 || // - diff --git a/public/app/plugins/datasource/graphite/parser.js b/public/app/plugins/datasource/graphite/parser.js index 242068f9d85..2ff15cda5b0 100644 --- a/public/app/plugins/datasource/graphite/parser.js +++ b/public/app/plugins/datasource/graphite/parser.js @@ -142,13 +142,13 @@ define([ name: this.consumeToken().value, }; - // consume left paranthesis + // consume left parenthesis this.consumeToken(); node.params = this.functionParameters(); if (!this.match(')')) { - this.errorMark('Expected closing paranthesis'); + this.errorMark('Expected closing parenthesis'); } this.consumeToken(); diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index a8656114fdf..f5b01fad79f 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -28,38 +28,43 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { this.supportAnnotations = true; this.supportMetrics = true; - this.editorSrc = 'app/features/influxdb/partials/query.editor.html'; - this.annotationEditorSrc = 'app/features/influxdb/partials/annotations.editor.html'; } InfluxDatasource.prototype.query = function(options) { var timeFilter = getTimeFilter(options); + var i, y; - var promises = _.map(options.targets, function(target) { - if (target.hide) { - return []; - } + var allQueries = _.map(options.targets, function(target) { + if (target.hide) { return []; } // build query var queryBuilder = new InfluxQueryBuilder(target); - var query = queryBuilder.build(); - - // replace grafana variables - query = query.replace('$timeFilter', timeFilter); + var query = queryBuilder.build(); query = query.replace(/\$interval/g, (target.interval || options.interval)); + return query; - // replace templated variables - query = templateSrv.replace(query); + }).join("\n"); - var alias = target.alias ? templateSrv.replace(target.alias) : ''; + // replace grafana variables + allQueries = allQueries.replace(/\$timeFilter/g, timeFilter); - var handleResponse = _.partial(handleInfluxQueryResponse, alias); - return this._seriesQuery(query).then(handleResponse); + // replace templated variables + allQueries = templateSrv.replace(allQueries, options.scopedVars); + return this._seriesQuery(allQueries).then(function(data) { + if (!data || !data.results || !data.results[0].series) { + return []; + } - }, this); + var seriesList = []; + for (i = 0; i < data.results.length; i++) { + var alias = (options.targets[i] || {}).alias; + var targetSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }).getTimeSeries(); + for (y = 0; y < targetSeries.length; y++) { + seriesList.push(targetSeries[y]); + } + } - return $q.all(promises).then(function(results) { - return { data: _.flatten(results) }; + return { data: seriesList }; }); }; @@ -123,7 +128,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { } InfluxDatasource.prototype._seriesQuery = function(query) { - return this._influxRequest('GET', '/query', {q: query}); + return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); }; InfluxDatasource.prototype.testDatasource = function() { @@ -176,13 +181,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { return deferred.promise; }; - function handleInfluxQueryResponse(alias, data) { - if (!data || !data.results || !data.results[0].series) { - return []; - } - return new InfluxSeries({ series: data.results[0].series, alias: alias }).getTimeSeries(); - } - function getTimeFilter(options) { var from = getInfluxTime(options.range.from); var until = getInfluxTime(options.range.to); diff --git a/public/app/plugins/datasource/influxdb/funcEditor.js b/public/app/plugins/datasource/influxdb/funcEditor.js index 12731c0859d..ae4248cf2a6 100644 --- a/public/app/plugins/datasource/influxdb/funcEditor.js +++ b/public/app/plugins/datasource/influxdb/funcEditor.js @@ -11,22 +11,36 @@ function (angular, _, $) { .directive('influxdbFuncEditor', function($compile) { var funcSpanTemplate = '{{target.function}}('; + 'data-toggle="dropdown">{{field.func}}('; var paramTemplate = ''; + var functionList = [ + 'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', + 'derivative', 'stddev', 'first', 'last', 'difference' + ]; + + var functionMenu = _.map(functionList, function(func) { + return { text: func, click: "changeFunction('" + func + "');" }; + }); + return { restrict: 'A', + scope: { + field: "=", + getFields: "&", + onChange: "&", + }, link: function postLink($scope, elem) { var $funcLink = $(funcSpanTemplate); - $scope.functionMenu = _.map($scope.functions, function(func) { - return { - text: func, - click: "changeFunction('" + func + "');" - }; - }); + $scope.functionMenu = functionMenu; + + $scope.changeFunction = function(func) { + $scope.field.func = func; + $scope.onChange(); + }; function clickFuncParam() { /*jshint validthis:true */ @@ -34,7 +48,7 @@ function (angular, _, $) { var $link = $(this); var $input = $link.next(); - $input.val($scope.target.column); + $input.val($scope.field.name); $input.css('width', ($link.width() + 16) + 'px'); $link.hide(); @@ -58,8 +72,8 @@ function (angular, _, $) { if ($input.val() !== '') { $link.text($input.val()); - $scope.target.column = $input.val(); - $scope.$apply($scope.get_data); + $scope.field.name = $input.val(); + $scope.$apply($scope.onChange()); } $input.hide(); @@ -83,8 +97,10 @@ function (angular, _, $) { $input.attr('data-provide', 'typeahead'); $input.typeahead({ - source: function () { - return $scope.listColumns.apply(null, arguments); + source: function (query, callback) { + return $scope.getFields().then(function(results) { + callback(results); + }); }, minLength: 0, items: 20, @@ -108,7 +124,7 @@ function (angular, _, $) { function addElementsAndCompile() { $funcLink.appendTo(elem); - var $paramLink = $('value'); + var $paramLink = $('' + $scope.field.name + ''); var $input = $(paramTemplate); $paramLink.appendTo(elem); diff --git a/public/app/plugins/datasource/influxdb/influxSeries.js b/public/app/plugins/datasource/influxdb/influxSeries.js index a4a21b1f6f0..43fb484e9ce 100644 --- a/public/app/plugins/datasource/influxdb/influxSeries.js +++ b/public/app/plugins/datasource/influxdb/influxSeries.js @@ -15,30 +15,40 @@ function (_) { p.getTimeSeries = function() { var output = []; var self = this; + var i, j; if (self.series.length === 0) { return output; } _.each(self.series, function(series) { - var datapoints = []; - for (var i = 0; i < series.values.length; i++) { - datapoints[i] = [series.values[i][1], new Date(series.values[i][0]).getTime()]; + var columns = series.columns.length; + var tags = _.map(series.tags, function(value, key) { + return key + ': ' + value; + }); + + for (j = 1; j < columns; j++) { + var seriesName = series.name; + var columnName = series.columns[j]; + if (columnName !== 'value') { + seriesName = seriesName + '.' + columnName; + } + + if (self.alias) { + seriesName = self._getSeriesName(series); + } else if (series.tags) { + seriesName = seriesName + ' {' + tags.join(', ') + '}'; + } + + var datapoints = []; + if (series.values) { + for (i = 0; i < series.values.length; i++) { + datapoints[i] = [series.values[i][j], series.values[i][0]]; + } + } + + output.push({ target: seriesName, datapoints: datapoints}); } - - var seriesName = series.name; - - if (self.alias) { - seriesName = self._getSeriesName(series); - } else if (series.tags) { - var tags = _.map(series.tags, function(value, key) { - return key + ': ' + value; - }); - - seriesName = seriesName + ' {' + tags.join(', ') + '}'; - } - - output.push({ target: seriesName, datapoints: datapoints }); }); return output; diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 1f7bf27a2a1..66c39fe7b69 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -1,11 +1,3 @@ -
    -
    Data source implementation: Alpha stage
    -
      -
    • This data source implementation is not complete, a lot is not working and implemented yet
    • -
    • Updates can be tracked, and feedback directed here #1525.
    • -
    -
    -

    diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 9a9fca802ea..d63e6853831 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -59,16 +59,18 @@ - + @@ -108,14 +110,14 @@
    -
    -
      +
      +
      • -
      • - GROUP BY +
      • + GROUP BY
      • diff --git a/public/app/plugins/datasource/influxdb/queryBuilder.js b/public/app/plugins/datasource/influxdb/queryBuilder.js index 37106d0729e..717ae22847f 100644 --- a/public/app/plugins/datasource/influxdb/queryBuilder.js +++ b/public/app/plugins/datasource/influxdb/queryBuilder.js @@ -8,6 +8,18 @@ function (_) { this.target = target; } + function renderTagCondition (tag, index) { + var str = ""; + if (index > 0) { + str = (tag.condition || 'AND') + ' '; + } + + if (tag.value && tag.value[0] === '/' && tag.value[tag.value.length - 1] === '/') { + return str + '"' +tag.key + '"' + ' =~ ' + tag.value; + } + return str + '"' + tag.key + '"' + " = '" + tag.value + "'"; + } + var p = InfluxQueryBuilder.prototype; p.build = function() { @@ -20,12 +32,15 @@ function (_) { if (type === 'TAG_KEYS') { query = 'SHOW TAG KEYS'; - measurement= this.target.measurement; + measurement = this.target.measurement; } else if (type === 'TAG_VALUES') { query = 'SHOW TAG VALUES'; - measurement= this.target.measurement; + measurement = this.target.measurement; } else if (type === 'MEASUREMENTS') { query = 'SHOW MEASUREMENTS'; + } else if (type === 'FIELDS') { + query = 'SHOW FIELD KEYS FROM "' + this.target.measurement + '"'; + return query; } if (measurement) { @@ -42,12 +57,12 @@ function (_) { if (tag.key === withKey) { return memo; } - memo.push(' ' + tag.key + '=' + "'" + tag.value + "'"); + memo.push(renderTagCondition(tag, memo.length)); return memo; }, []); if (whereConditions.length > 0) { - query += ' WHERE' + whereConditions.join('AND'); + query += ' WHERE ' + whereConditions.join(' '); } } @@ -61,26 +76,36 @@ function (_) { throw "Metric measurement is missing"; } - var query = 'SELECT '; - var measurement = target.measurement; - var aggregationFunc = target.function || 'mean'; + if (!target.fields) { + target.fields = [{name: 'value', func: target.function || 'mean'}]; + } + var query = 'SELECT '; + var i; + for (i = 0; i < target.fields.length; i++) { + var field = target.fields[i]; + if (i > 0) { + query += ', '; + } + query += field.func + '(' + field.name + ')'; + } + + var measurement = target.measurement; if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) { measurement = '"' + measurement+ '"'; } - query += aggregationFunc + '(value)'; query += ' FROM ' + measurement + ' WHERE '; - var conditions = _.map(target.tags, function(tag) { - return tag.key + '=' + "'" + tag.value + "' "; + var conditions = _.map(target.tags, function(tag, index) { + return renderTagCondition(tag, index); }); - conditions.push('$timeFilter'); - query += conditions.join('AND '); + query += conditions.join(' '); + query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter'; query += ' GROUP BY time($interval)'; if (target.groupByTags && target.groupByTags.length > 0) { - query += ', ' + target.groupByTags.join(); + query += ', "' + target.groupByTags.join('", "') + '"'; } if (target.fill) { diff --git a/public/app/plugins/datasource/influxdb/queryCtrl.js b/public/app/plugins/datasource/influxdb/queryCtrl.js index a6d530fe68f..a57737cba93 100644 --- a/public/app/plugins/datasource/influxdb/queryCtrl.js +++ b/public/app/plugins/datasource/influxdb/queryCtrl.js @@ -10,20 +10,14 @@ function (angular, _, InfluxQueryBuilder) { module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) { - $scope.functionList = [ - 'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median', - 'derivative', 'stddev', 'first', 'last', 'difference' - ]; - - $scope.functionMenu = _.map($scope.functionList, function(func) { - return { text: func, click: "changeFunction('" + func + "');" }; - }); - $scope.init = function() { var target = $scope.target; - target.function = target.function || 'mean'; target.tags = target.tags || []; target.groupByTags = target.groupByTags || []; + target.fields = target.fields || [{ + name: 'value', + func: target.function || 'mean' + }]; $scope.queryBuilder = new InfluxQueryBuilder(target); @@ -33,13 +27,15 @@ function (angular, _, InfluxQueryBuilder) { $scope.measurementSegment = new MetricSegment(target.measurement); } + $scope.addFieldSegment = MetricSegment.newPlusButton(); + $scope.tagSegments = []; _.each(target.tags, function(tag) { if (tag.condition) { $scope.tagSegments.push(MetricSegment.newCondition(tag.condition)); } $scope.tagSegments.push(new MetricSegment({value: tag.key, type: 'key', cssClass: 'query-segment-key' })); - $scope.tagSegments.push(new MetricSegment({fake: true, value: "=", cssClass: 'query-segment-operator'})); + $scope.tagSegments.push(new MetricSegment.newOperator("=")); $scope.tagSegments.push(new MetricSegment({value: tag.value, type: 'value', cssClass: 'query-segment-value'})); }); @@ -94,6 +90,18 @@ function (angular, _, InfluxQueryBuilder) { $scope.$parent.get_data(); }; + $scope.getFields = function() { + var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS'); + return $scope.datasource.metricFindQuery(fieldsQuery) + .then(function(results) { + var values = _.pluck(results, 'text'); + if ($scope.target.fields.length > 1) { + values.splice(0, 0, "-- remove from select --"); + } + return values; + }); + }; + $scope.toggleQueryMode = function () { $scope.target.rawQuery = !$scope.target.rawQuery; }; @@ -159,6 +167,25 @@ function (angular, _, InfluxQueryBuilder) { .then(null, $scope.handleQueryError); }; + $scope.getFieldSegments = function() { + var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS'); + return $scope.datasource.metricFindQuery(fieldsQuery) + .then($scope.transformToSegments) + .then(null, $scope.handleQueryError); + }; + + $scope.addField = function() { + $scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'}); + _.extend($scope.addFieldSegment, MetricSegment.newPlusButton()); + }; + + $scope.fieldChanged = function(field) { + if (field.name === '-- remove from select --') { + $scope.target.fields = _.without($scope.target.fields, field); + } + $scope.get_data(); + }; + $scope.getGroupByTagSegments = function(segment) { var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS'); @@ -177,6 +204,7 @@ function (angular, _, InfluxQueryBuilder) { $scope.tagSegmentUpdated = function(segment, index) { $scope.tagSegments[index] = segment; + // handle remove tag condition if (segment.value === $scope.removeTagFilterSegment.value) { $scope.tagSegments.splice(index, 3); if ($scope.tagSegments.length === 0) { @@ -193,7 +221,7 @@ function (angular, _, InfluxQueryBuilder) { if (index > 2) { $scope.tagSegments.splice(index, 0, MetricSegment.newCondition('AND')); } - $scope.tagSegments.push(MetricSegment.newFake('=', 'operator', 'query-segment-operator')); + $scope.tagSegments.push(MetricSegment.newOperator('=')); $scope.tagSegments.push(MetricSegment.newFake('select tag value', 'value', 'query-segment-value')); segment.type = 'key'; segment.cssClass = 'query-segment-key'; @@ -210,7 +238,7 @@ function (angular, _, InfluxQueryBuilder) { $scope.rebuildTargetTagConditions = function() { var tags = []; var tagIndex = 0; - _.each($scope.tagSegments, function(segment2) { + _.each($scope.tagSegments, function(segment2, index) { if (segment2.type === 'key') { if (tags.length === 0) { tags.push({}); @@ -219,6 +247,7 @@ function (angular, _, InfluxQueryBuilder) { } else if (segment2.type === 'value') { tags[tagIndex].value = segment2.value; + $scope.tagSegments[index-1] = $scope.getTagValueOperator(segment2.value); } else if (segment2.type === 'condition') { tags.push({ condition: segment2.value }); @@ -230,6 +259,14 @@ function (angular, _, InfluxQueryBuilder) { $scope.$parent.get_data(); }; + $scope.getTagValueOperator = function(tagValue) { + if (tagValue[0] === '/' && tagValue[tagValue.length - 1] === '/') { + return MetricSegment.newOperator('=~'); + } + + return MetricSegment.newOperator('='); + }; + function MetricSegment(options) { if (options === '*' || options.value === '*') { this.value = '*'; @@ -265,6 +302,10 @@ function (angular, _, InfluxQueryBuilder) { return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' }); }; + MetricSegment.newOperator = function(op) { + return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' }); + }; + MetricSegment.newPlusButton = function() { return new MetricSegment({fake: true, html: '', type: 'plus-button' }); }; diff --git a/public/app/plugins/datasource/kairosdb/datasource.js b/public/app/plugins/datasource/kairosdb/datasource.js new file mode 100644 index 00000000000..d0cddca3bec --- /dev/null +++ b/public/app/plugins/datasource/kairosdb/datasource.js @@ -0,0 +1,474 @@ +define([ + 'angular', + 'lodash', + 'kbn', + './queryCtrl', +], +function (angular, _, kbn) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.factory('KairosDBDatasource', function($q, $http, templateSrv) { + + function KairosDBDatasource(datasource) { + this.type = datasource.type; + this.url = datasource.url; + this.name = datasource.name; + this.supportMetrics = true; + } + + // Called once per panel (graph) + KairosDBDatasource.prototype.query = function(options) { + var start = options.range.from; + var end = options.range.to; + + var queries = _.compact(_.map(options.targets, _.partial(convertTargetToQuery, options))); + var plotParams = _.compact(_.map(options.targets, function(target) { + var alias = target.alias; + if (typeof target.alias === 'undefined' || target.alias === "") { + alias = target.metric; + } + + if (!target.hide) { + return { alias: alias, exouter: target.exOuter }; + } + else { + return null; + } + })); + + var handleKairosDBQueryResponseAlias = _.partial(handleKairosDBQueryResponse, plotParams); + + // No valid targets, return the empty result to save a round trip. + if (_.isEmpty(queries)) { + var d = $q.defer(); + d.resolve({ data: [] }); + return d.promise; + } + + return this.performTimeSeriesQuery(queries, start, end).then(handleKairosDBQueryResponseAlias, handleQueryError); + }; + + /////////////////////////////////////////////////////////////////////// + /// Query methods + /////////////////////////////////////////////////////////////////////// + + KairosDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) { + var reqBody = { + metrics: queries, + cache_time: 0 + }; + + convertToKairosTime(start, reqBody, 'start'); + convertToKairosTime(end, reqBody, 'end'); + + var options = { + method: 'POST', + url: this.url + '/api/v1/datapoints/query', + data: reqBody + }; + + return $http(options); + }; + + /** + * Gets the list of metrics + * @returns {*|Promise} + */ + KairosDBDatasource.prototype.performMetricSuggestQuery = function() { + var options = { + url : this.url + '/api/v1/metricnames', + method : 'GET' + }; + + return $http(options).then(function(response) { + if (!response.data) { + return []; + } + return response.data.results; + }); + }; + + KairosDBDatasource.prototype.performListTagNames = function() { + var options = { + url : this.url + '/api/v1/tagnames', + method : 'GET' + }; + + return $http(options).then(function(response) { + if (!response.data) { + return []; + } + return response.data.results; + }); + }; + + KairosDBDatasource.prototype.performListTagValues = function() { + var options = { + url : this.url + '/api/v1/tagvalues', + method : 'GET' + }; + + return $http(options).then(function(response) { + if (!response.data) { + return []; + } + return response.data.results; + }); + }; + + KairosDBDatasource.prototype.performTagSuggestQuery = function(metricname) { + var options = { + url : this.url + '/api/v1/datapoints/query/tags', + method : 'POST', + data : { + metrics : [{ name : metricname }], + cache_time : 0, + start_absolute: 0 + } + }; + + return $http(options).then(function(response) { + if (!response.data) { + return []; + } + else { + return response.data.queries[0].results[0]; + } + }); + }; + + KairosDBDatasource.prototype.metricFindQuery = function(query) { + function format(results, query) { + return _.chain(results) + .filter(function(result) { + return result.indexOf(query) >= 0; + }) + .map(function(result) { + return { + text: result, + expandable: true + }; + }) + .value(); + } + + var interpolated; + try { + interpolated = templateSrv.replace(query); + } + catch (err) { + return $q.reject(err); + } + + var metrics_regex = /metrics\((.*)\)/; + var tag_names_regex = /tag_names\((.*)\)/; + var tag_values_regex = /tag_values\((.*)\)/; + + var metrics_query = interpolated.match(metrics_regex); + if (metrics_query) { + return this.performMetricSuggestQuery().then(function(metrics) { + return format(metrics, metrics_query[1]); + }); + } + + var tag_names_query = interpolated.match(tag_names_regex); + if (tag_names_query) { + return this.performListTagNames().then(function(tag_names) { + return format(tag_names, tag_names_query[1]); + }); + } + + var tag_values_query = interpolated.match(tag_values_regex); + if (tag_values_query) { + return this.performListTagValues().then(function(tag_values) { + return format(tag_values, tag_values_query[1]); + }); + } + }; + + ///////////////////////////////////////////////////////////////////////// + /// Formatting methods + //////////////////////////////////////////////////////////////////////// + + /** + * Requires a verion of KairosDB with every CORS defects fixed + * @param results + * @returns {*} + */ + function handleQueryError(results) { + if (results.data.errors && !_.isEmpty(results.data.errors)) { + var errors = { + message: results.data.errors[0] + }; + return $q.reject(errors); + } + else { + return $q.reject(results); + } + } + + function handleKairosDBQueryResponse(plotParams, results) { + var output = []; + var index = 0; + _.each(results.data.queries, function(series) { + _.each(series.results, function(result) { + var target = plotParams[index].alias; + var details = " ( "; + + _.each(result.group_by, function(element) { + if (element.name === "tag") { + _.each(element.group, function(value, key) { + details += key + "=" + value + " "; + }); + } + else if (element.name === "value") { + details += 'value_group=' + element.group.group_number + " "; + } + else if (element.name === "time") { + details += 'time_group=' + element.group.group_number + " "; + } + }); + + details += ") "; + + if (details !== " ( ) ") { + target += details; + } + + var datapoints = []; + + for (var i = 0; i < result.values.length; i++) { + var t = Math.floor(result.values[i][0]); + var v = result.values[i][1]; + datapoints[i] = [v, t]; + } + if (plotParams[index].exouter) { + datapoints = new PeakFilter(datapoints, 10); + } + output.push({ target: target, datapoints: datapoints }); + }); + + index++; + }); + + return { data: _.flatten(output) }; + } + + function convertTargetToQuery(options, target) { + if (!target.metric || target.hide) { + return null; + } + + var query = { + name: templateSrv.replace(target.metric) + }; + + query.aggregators = []; + + if (target.downsampling !== '(NONE)') { + query.aggregators.push({ + name: target.downsampling, + align_sampling: true, + align_start_time: true, + sampling: KairosDBDatasource.prototype.convertToKairosInterval(target.sampling || options.interval) + }); + } + + if (target.horizontalAggregators) { + _.each(target.horizontalAggregators, function(chosenAggregator) { + var returnedAggregator = { + name:chosenAggregator.name + }; + + if (chosenAggregator.sampling_rate) { + returnedAggregator.sampling = KairosDBDatasource.prototype.convertToKairosInterval(chosenAggregator.sampling_rate); + returnedAggregator.align_sampling = true; + returnedAggregator.align_start_time =true; + } + + if (chosenAggregator.unit) { + returnedAggregator.unit = chosenAggregator.unit + 's'; + } + + if (chosenAggregator.factor && chosenAggregator.name === 'div') { + returnedAggregator.divisor = chosenAggregator.factor; + } + else if (chosenAggregator.factor && chosenAggregator.name === 'scale') { + returnedAggregator.factor = chosenAggregator.factor; + } + + if (chosenAggregator.percentile) { + returnedAggregator.percentile = chosenAggregator.percentile; + } + query.aggregators.push(returnedAggregator); + }); + } + + if (_.isEmpty(query.aggregators)) { + delete query.aggregators; + } + + if (target.tags) { + query.tags = angular.copy(target.tags); + _.forOwn(query.tags, function(value, key) { + query.tags[key] = _.map(value, function(tag) { return templateSrv.replace(tag); }); + }); + } + + if (target.groupByTags || target.nonTagGroupBys) { + query.group_by = []; + if (target.groupByTags) { + query.group_by.push({ + name: "tag", + tags: _.map(angular.copy(target.groupByTags), function(tag) { return templateSrv.replace(tag); }) + }); + } + + if (target.nonTagGroupBys) { + _.each(target.nonTagGroupBys, function(rawGroupBy) { + var formattedGroupBy = angular.copy(rawGroupBy); + if (formattedGroupBy.name === 'time') { + formattedGroupBy.range_size = KairosDBDatasource.prototype.convertToKairosInterval(formattedGroupBy.range_size); + } + query.group_by.push(formattedGroupBy); + }); + } + } + return query; + } + + /////////////////////////////////////////////////////////////////////// + /// Time conversion functions specifics to KairosDB + ////////////////////////////////////////////////////////////////////// + + KairosDBDatasource.prototype.convertToKairosInterval = function(intervalString) { + intervalString = templateSrv.replace(intervalString); + + var interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/; + var interval_regex_ms = /(\d+(?:\.\d+)?)(ms)/; + var matches = intervalString.match(interval_regex_ms); + if (!matches) { + matches = intervalString.match(interval_regex); + } + if (!matches) { + throw new Error('Invalid interval string, expecting a number followed by one of "y M w d h m s ms"'); + } + + var value = matches[1]; + var unit = matches[2]; + if (value%1 !== 0) { + if (unit === 'ms') { + throw new Error('Invalid interval value, cannot be smaller than the millisecond'); + } + value = Math.round(kbn.intervals_in_seconds[unit] * value * 1000); + unit = 'ms'; + } + + return { + value: value, + unit: convertToKairosDBTimeUnit(unit) + }; + }; + + function convertToKairosTime(date, response_obj, start_stop_name) { + var name; + + if (_.isString(date)) { + if (date === 'now') { + return; + } + else if (date.indexOf('now-') >= 0) { + date = date.substring(4); + name = start_stop_name + "_relative"; + var re_date = /(\d+)\s*(\D+)/; + var result = re_date.exec(date); + + if (result) { + var value = result[1]; + var unit = result[2]; + + response_obj[name] = { + value: value, + unit: convertToKairosDBTimeUnit(unit) + }; + return; + } + console.log("Unparseable date", date); + return; + } + + date = kbn.parseDate(date); + } + + if (_.isDate(date)) { + name = start_stop_name + "_absolute"; + response_obj[name] = date.getTime(); + return; + } + + console.log("Date is neither string nor date"); + } + + function convertToKairosDBTimeUnit(unit) { + switch (unit) { + case 'ms': + return 'milliseconds'; + case 's': + return 'seconds'; + case 'm': + return 'minutes'; + case 'h': + return 'hours'; + case 'd': + return 'days'; + case 'w': + return 'weeks'; + case 'M': + return 'months'; + case 'y': + return 'years'; + default: + console.log("Unknown unit ", unit); + return ''; + } + } + + function PeakFilter(dataIn, limit) { + var datapoints = dataIn; + var arrLength = datapoints.length; + if (arrLength <= 3) { + return datapoints; + } + var LastIndx = arrLength - 1; + + // Check first point + var prvDelta = Math.abs((datapoints[1][0] - datapoints[0][0]) / datapoints[0][0]); + var nxtDelta = Math.abs((datapoints[1][0] - datapoints[2][0]) / datapoints[2][0]); + if (prvDelta >= limit && nxtDelta < limit) { + datapoints[0][0] = datapoints[1][0]; + } + + // Check last point + prvDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx - 2][0]) / datapoints[LastIndx - 2][0]); + nxtDelta = Math.abs((datapoints[LastIndx - 1][0] - datapoints[LastIndx][0]) / datapoints[LastIndx][0]); + if (prvDelta >= limit && nxtDelta < limit) { + datapoints[LastIndx][0] = datapoints[LastIndx - 1][0]; + } + + for (var i = 1; i < arrLength - 1; i++) { + prvDelta = Math.abs((datapoints[i][0] - datapoints[i - 1][0]) / datapoints[i - 1][0]); + nxtDelta = Math.abs((datapoints[i][0] - datapoints[i + 1][0]) / datapoints[i + 1][0]); + if (prvDelta >= limit && nxtDelta >= limit) { + datapoints[i][0] = (datapoints[i - 1][0] + datapoints[i + 1][0]) / 2; + } + } + + return datapoints; + } + + return KairosDBDatasource; + }); + +}); diff --git a/public/app/plugins/datasource/kairosdb/partials/config.html b/public/app/plugins/datasource/kairosdb/partials/config.html new file mode 100644 index 00000000000..384edeaeafe --- /dev/null +++ b/public/app/plugins/datasource/kairosdb/partials/config.html @@ -0,0 +1 @@ +
        diff --git a/public/app/plugins/datasource/kairosdb/partials/query.editor.html b/public/app/plugins/datasource/kairosdb/partials/query.editor.html new file mode 100644 index 00000000000..c72a242165b --- /dev/null +++ b/public/app/plugins/datasource/kairosdb/partials/query.editor.html @@ -0,0 +1,384 @@ +
        +
        + +
        + + +
          +
        • + + + +
        • +
        • + Metric +
        • +
        • + + + + +
        • +
        • + Alias +
        • +
        • + +
        • +
        • +  Peak filter + +
        • +
        + +
        +
        + + +
        + +
        +
        + + +
        +
        + + +
        + +
        +
        +
        +
        + +
        +
        +
          +
        • + +
        • + +
        • + Downsampling with +
        • +
        • + +
        • + + +
        • + every +
        • +
        • + + + + +
        • +
        +
        +
        +
        diff --git a/public/app/plugins/datasource/kairosdb/plugin.json b/public/app/plugins/datasource/kairosdb/plugin.json new file mode 100644 index 00000000000..bdbf27c5fa8 --- /dev/null +++ b/public/app/plugins/datasource/kairosdb/plugin.json @@ -0,0 +1,17 @@ +{ + "pluginType": "datasource", + "name": "KairosDB", + + "type": "kairosdb", + "serviceName": "KairosDBDatasource", + + "module": "plugins/datasource/kairosdb/datasource", + + "partials": { + "config": "app/plugins/datasource/kairosdb/partials/config.html", + "query": "app/plugins/datasource/kairosdb/partials/query.editor.html" + }, + + "metrics": true, + "annotations": false +} diff --git a/public/app/plugins/datasource/kairosdb/queryCtrl.js b/public/app/plugins/datasource/kairosdb/queryCtrl.js new file mode 100644 index 00000000000..9e8c5817dd1 --- /dev/null +++ b/public/app/plugins/datasource/kairosdb/queryCtrl.js @@ -0,0 +1,367 @@ +define([ + 'angular', + 'lodash' +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + var metricList = []; + var tagList = []; + + module.controller('KairosDBQueryCtrl', function($scope) { + + $scope.init = function() { + $scope.panel.stack = false; + if (!$scope.panel.downsampling) { + $scope.panel.downsampling = 'avg'; + } + if (!$scope.target.downsampling) { + $scope.target.downsampling = $scope.panel.downsampling; + $scope.target.sampling = $scope.panel.sampling; + } + $scope.target.errors = validateTarget($scope.target); + }; + + $scope.targetBlur = function() { + $scope.target.errors = validateTarget($scope.target); + if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) { + $scope.oldTarget = angular.copy($scope.target); + $scope.get_data(); + } + }; + + $scope.panelBlur = function() { + _.each($scope.panel.targets, function(target) { + target.downsampling = $scope.panel.downsampling; + target.sampling = $scope.panel.sampling; + }); + $scope.get_data(); + }; + + $scope.duplicate = function() { + var clone = angular.copy($scope.target); + $scope.panel.targets.push(clone); + }; + + $scope.moveMetricQuery = function(fromIndex, toIndex) { + _.move($scope.panel.targets, fromIndex, toIndex); + }; + + $scope.suggestMetrics = function(query, callback) { + if (!_.isEmpty(metricList)) { + return metricList; + } + else { + $scope.datasource.performMetricSuggestQuery().then(function(result) { + metricList = result; + callback(metricList); + }); + } + }; + + $scope.suggestTagKeys = function(query, callback) { + if (!_.isEmpty(tagList)) { + var result = _.find(tagList, { name : $scope.target.metric }); + + if (!_.isEmpty(result)) { + return _.keys(result.tags); + } + } + + $scope.datasource.performTagSuggestQuery($scope.target.metric).then(function(result) { + if (!_.isEmpty(result)) { + tagList.push(result); + callback(_.keys(result.tags)); + } + }); + }; + + $scope.suggestTagValues = function(query, callback) { + if (!_.isEmpty(tagList)) { + var result = _.find(tagList, { name : $scope.target.metric }); + + if (!_.isEmpty(result)) { + return result.tags[$scope.target.currentTagKey]; + } + } + + $scope.datasource.performTagSuggestQuery($scope.target.metric).then(function(result) { + if (!_.isEmpty(result)) { + tagList.push(result); + callback(result.tags[$scope.target.currentTagKey]); + } + }); + }; + + // Filter metric by tag + $scope.addFilterTag = function() { + if (!$scope.addFilterTagMode) { + $scope.addFilterTagMode = true; + $scope.validateFilterTag(); + return; + } + + if (!$scope.target.tags) { + $scope.target.tags = {}; + } + + $scope.validateFilterTag(); + if (!$scope.target.errors.tags) { + if (!_.has($scope.target.tags, $scope.target.currentTagKey)) { + $scope.target.tags[$scope.target.currentTagKey] = []; + } + $scope.target.tags[$scope.target.currentTagKey].push($scope.target.currentTagValue); + $scope.target.currentTagKey = ''; + $scope.target.currentTagValue = ''; + $scope.targetBlur(); + } + + $scope.addFilterTagMode = false; + }; + + $scope.removeFilterTag = function(key) { + delete $scope.target.tags[key]; + if (_.size($scope.target.tags) === 0) { + $scope.target.tags = null; + } + $scope.targetBlur(); + }; + + $scope.validateFilterTag = function() { + $scope.target.errors.tags = null; + if (!$scope.target.currentTagKey || !$scope.target.currentTagValue) { + $scope.target.errors.tags = "You must specify a tag name and value."; + } + }; + + ////////////////////////////// + // GROUP BY + ////////////////////////////// + + $scope.addGroupBy = function() { + if (!$scope.addGroupByMode) { + $scope.addGroupByMode = true; + $scope.target.currentGroupByType = 'tag'; + $scope.isTagGroupBy = true; + $scope.validateGroupBy(); + return; + } + $scope.validateGroupBy(); + // nb: if error is found, means that user clicked on cross : cancels input + if (_.isEmpty($scope.target.errors.groupBy)) { + if ($scope.isTagGroupBy) { + if (!$scope.target.groupByTags) { + $scope.target.groupByTags = []; + } + if (!_.contains($scope.target.groupByTags, $scope.target.groupBy.tagKey)) { + $scope.target.groupByTags.push($scope.target.groupBy.tagKey); + $scope.targetBlur(); + } + $scope.target.groupBy.tagKey = ''; + } + else { + if (!$scope.target.nonTagGroupBys) { + $scope.target.nonTagGroupBys = []; + } + var groupBy = { + name: $scope.target.currentGroupByType + }; + if ($scope.isValueGroupBy) {groupBy.range_size = $scope.target.groupBy.valueRange;} + else if ($scope.isTimeGroupBy) { + groupBy.range_size = $scope.target.groupBy.timeInterval; + groupBy.group_count = $scope.target.groupBy.groupCount; + } + $scope.target.nonTagGroupBys.push(groupBy); + } + $scope.targetBlur(); + } + $scope.isTagGroupBy = false; + $scope.isValueGroupBy = false; + $scope.isTimeGroupBy = false; + $scope.addGroupByMode = false; + }; + + $scope.removeGroupByTag = function(index) { + $scope.target.groupByTags.splice(index, 1); + if (_.size($scope.target.groupByTags) === 0) { + $scope.target.groupByTags = null; + } + $scope.targetBlur(); + }; + + $scope.removeNonTagGroupBy = function(index) { + $scope.target.nonTagGroupBys.splice(index, 1); + if (_.size($scope.target.nonTagGroupBys) === 0) { + $scope.target.nonTagGroupBys = null; + } + $scope.targetBlur(); + }; + + $scope.changeGroupByInput = function() { + $scope.isTagGroupBy = $scope.target.currentGroupByType === 'tag'; + $scope.isValueGroupBy = $scope.target.currentGroupByType === 'value'; + $scope.isTimeGroupBy = $scope.target.currentGroupByType === 'time'; + $scope.validateGroupBy(); + }; + + $scope.validateGroupBy = function() { + delete $scope.target.errors.groupBy; + var errors = {}; + $scope.isGroupByValid = true; + if ($scope.isTagGroupBy) { + if (!$scope.target.groupBy.tagKey) { + $scope.isGroupByValid = false; + errors.tagKey = 'You must supply a tag name'; + } + } + + if ($scope.isValueGroupBy) { + if (!$scope.target.groupBy.valueRange || !isInt($scope.target.groupBy.valueRange)) { + errors.valueRange = "Range must be an integer"; + $scope.isGroupByValid = false; + } + } + + if ($scope.isTimeGroupBy) { + try { + $scope.datasource.convertToKairosInterval($scope.target.groupBy.timeInterval); + } catch (err) { + errors.timeInterval = err.message; + $scope.isGroupByValid = false; + } + if (!$scope.target.groupBy.groupCount || !isInt($scope.target.groupBy.groupCount)) { + errors.groupCount = "Group count must be an integer"; + $scope.isGroupByValid = false; + } + } + + if (!_.isEmpty(errors)) { + $scope.target.errors.groupBy = errors; + } + }; + + function isInt(n) { + return parseInt(n) % 1 === 0; + } + + ////////////////////////////// + // HORIZONTAL AGGREGATION + ////////////////////////////// + + $scope.addHorizontalAggregator = function() { + if (!$scope.addHorizontalAggregatorMode) { + $scope.addHorizontalAggregatorMode = true; + $scope.target.currentHorizontalAggregatorName = 'avg'; + $scope.hasSamplingRate = true; + $scope.validateHorizontalAggregator(); + return; + } + + $scope.validateHorizontalAggregator(); + // nb: if error is found, means that user clicked on cross : cancels input + if (_.isEmpty($scope.target.errors.horAggregator)) { + if (!$scope.target.horizontalAggregators) { + $scope.target.horizontalAggregators = []; + } + var aggregator = { + name:$scope.target.currentHorizontalAggregatorName + }; + if ($scope.hasSamplingRate) {aggregator.sampling_rate = $scope.target.horAggregator.samplingRate;} + if ($scope.hasUnit) {aggregator.unit = $scope.target.horAggregator.unit;} + if ($scope.hasFactor) {aggregator.factor = $scope.target.horAggregator.factor;} + if ($scope.hasPercentile) {aggregator.percentile = $scope.target.horAggregator.percentile;} + $scope.target.horizontalAggregators.push(aggregator); + $scope.targetBlur(); + } + + $scope.addHorizontalAggregatorMode = false; + $scope.hasSamplingRate = false; + $scope.hasUnit = false; + $scope.hasFactor = false; + $scope.hasPercentile = false; + }; + + $scope.removeHorizontalAggregator = function(index) { + $scope.target.horizontalAggregators.splice(index, 1); + if (_.size($scope.target.horizontalAggregators) === 0) { + $scope.target.horizontalAggregators = null; + } + + $scope.targetBlur(); + }; + + $scope.changeHorAggregationInput = function() { + $scope.hasSamplingRate = _.contains(['avg','dev','max','min','sum','least_squares','count','percentile'], + $scope.target.currentHorizontalAggregatorName); + $scope.hasUnit = _.contains(['sampler','rate'], $scope.target.currentHorizontalAggregatorName); + $scope.hasFactor = _.contains(['div','scale'], $scope.target.currentHorizontalAggregatorName); + $scope.hasPercentile = 'percentile' === $scope.target.currentHorizontalAggregatorName; + $scope.validateHorizontalAggregator(); + }; + + $scope.validateHorizontalAggregator = function() { + delete $scope.target.errors.horAggregator; + var errors = {}; + $scope.isAggregatorValid = true; + + if ($scope.hasSamplingRate) { + try { + $scope.datasource.convertToKairosInterval($scope.target.horAggregator.samplingRate); + } catch (err) { + errors.samplingRate = err.message; + $scope.isAggregatorValid = false; + } + } + + if ($scope.hasFactor) { + if (!$scope.target.horAggregator.factor) { + errors.factor = 'You must supply a numeric value for this aggregator'; + $scope.isAggregatorValid = false; + } + else if (parseInt($scope.target.horAggregator.factor) === 0 && $scope.target.currentHorizontalAggregatorName === 'div') { + errors.factor = 'Cannot divide by 0'; + $scope.isAggregatorValid = false; + } + } + + if ($scope.hasPercentile) { + if (!$scope.target.horAggregator.percentile || + $scope.target.horAggregator.percentile<=0 || + $scope.target.horAggregator.percentile>1) { + errors.percentile = 'Percentile must be between 0 and 1'; + $scope.isAggregatorValid = false; + } + } + + if (!_.isEmpty(errors)) { + $scope.target.errors.horAggregator = errors; + } + }; + + $scope.alert = function(message) { + alert(message); + }; + + // Validation + function validateTarget(target) { + var errs = {}; + + if (!target.metric) { + errs.metric = "You must supply a metric name."; + } + + try { + if (target.sampling) { + $scope.datasource.convertToKairosInterval(target.sampling); + } + } catch (err) { + errs.sampling = err.message; + } + + return errs; + } + + }); + +}); diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index 1e60bf1e54e..d35c88fc45d 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -14,7 +14,6 @@ function (angular, _, kbn) { function OpenTSDBDatasource(datasource) { this.type = 'opentsdb'; - this.editorSrc = 'app/features/opentsdb/partials/query.editor.html'; this.url = datasource.url; this.name = datasource.name; this.supportMetrics = true; @@ -27,7 +26,8 @@ function (angular, _, kbn) { var qs = []; _.each(options.targets, function(target) { - qs.push(convertTargetToQuery(target, options.interval)); + if (!target.metric) { return; } + qs.push(convertTargetToQuery(target, options)); }); var queries = _.compact(qs); @@ -47,10 +47,13 @@ function (angular, _, kbn) { }); return this.performTimeSeriesQuery(queries, start, end).then(function(response) { - var metricToTargetMapping = mapMetricsToTargets(response.data, options.targets); + var metricToTargetMapping = mapMetricsToTargets(response.data, options); var result = _.map(response.data, function(metricData, index) { index = metricToTargetMapping[index]; - return transformMetricData(metricData, groupByTags, options.targets[index]); + if (index === -1) { + index = 0; + } + return transformMetricData(metricData, groupByTags, options.targets[index], options); }); return { data: result }; }); @@ -76,28 +79,101 @@ function (angular, _, kbn) { return backendSrv.datasourceRequest(options); }; - OpenTSDBDatasource.prototype.performSuggestQuery = function(query, type) { - var options = { - method: 'GET', - url: this.url + '/api/suggest', - params: { - type: type, - q: query - } - }; - return backendSrv.datasourceRequest(options).then(function(result) { + OpenTSDBDatasource.prototype._performSuggestQuery = function(query) { + return this._get('/api/suggest', {type: 'metrics', q: query, max: 1000}).then(function(result) { return result.data; }); }; + OpenTSDBDatasource.prototype._performMetricKeyValueLookup = function(metric, key) { + if(!metric || !key) { + return $q.when([]); + } + + var m = metric + "{" + key + "=*}"; + + return this._get('/api/search/lookup', {m: m}).then(function(result) { + result = result.data.results; + var tagvs = []; + _.each(result, function(r) { + tagvs.push(r.tags[key]); + }); + return tagvs; + }); + }; + + OpenTSDBDatasource.prototype._performMetricKeyLookup = function(metric) { + if(!metric) { return $q.when([]); } + + return this._get('/api/search/lookup', {m: metric}).then(function(result) { + result = result.data.results; + var tagks = []; + _.each(result, function(r) { + _.each(r.tags, function(tagv, tagk) { + if(tagks.indexOf(tagk) === -1) { + tagks.push(tagk); + } + }); + }); + return tagks; + }); + }; + + OpenTSDBDatasource.prototype._get = function(relativeUrl, params) { + return backendSrv.datasourceRequest({ + method: 'GET', + url: this.url + relativeUrl, + params: params, + }); + }; + + OpenTSDBDatasource.prototype.metricFindQuery = function(query) { + if (!query) { return $q.when([]); } + + var interpolated; + try { + interpolated = templateSrv.replace(query); + } + catch (err) { + return $q.reject(err); + } + + var responseTransform = function(result) { + return _.map(result, function(value) { + return {text: value}; + }); + }; + + var metrics_regex = /metrics\((.*)\)/; + var tag_names_regex = /tag_names\((.*)\)/; + var tag_values_regex = /tag_values\((.*),\s?(.*)\)/; + + var metrics_query = interpolated.match(metrics_regex); + if (metrics_query) { + return this._performSuggestQuery(metrics_query[1]).then(responseTransform); + } + + var tag_names_query = interpolated.match(tag_names_regex); + if (tag_names_query) { + return this._performMetricKeyLookup(tag_names_query[1]).then(responseTransform); + } + + var tag_values_query = interpolated.match(tag_values_regex); + if (tag_values_query) { + return this._performMetricKeyValueLookup(tag_values_query[1], tag_values_query[2]).then(responseTransform); + } + + return $q.when([]); + }; + OpenTSDBDatasource.prototype.testDatasource = function() { return this.performSuggestQuery('cpu', 'metrics').then(function () { return { status: "success", message: "Data source is working", title: "Success" }; }); }; - function transformMetricData(md, groupByTags, options) { - var metricLabel = createMetricLabel(md, options, groupByTags); + function transformMetricData(md, groupByTags, target, options) { + var metricLabel = createMetricLabel(md, target, groupByTags, options); var dps = []; // TSDB returns datapoints has a hash of ts => value. @@ -109,13 +185,13 @@ function (angular, _, kbn) { return { target: metricLabel, datapoints: dps }; } - function createMetricLabel(md, options, groupByTags) { - if (!_.isUndefined(options) && options.alias) { - var scopedVars = {}; + function createMetricLabel(md, target, groupByTags, options) { + if (target.alias) { + var scopedVars = _.clone(options.scopedVars || {}); _.each(md.tags, function(value, key) { scopedVars['tag_' + key] = {value: value}; }); - return templateSrv.replace(options.alias, scopedVars); + return templateSrv.replace(target.alias, scopedVars); } var label = md.metric; @@ -136,13 +212,13 @@ function (angular, _, kbn) { return label; } - function convertTargetToQuery(target, interval) { + function convertTargetToQuery(target, options) { if (!target.metric || target.hide) { return null; } var query = { - metric: templateSrv.replace(target.metric), + metric: templateSrv.replace(target.metric, options.scopedVars), aggregator: "avg" }; @@ -166,7 +242,7 @@ function (angular, _, kbn) { } if (!target.disableDownsampling) { - interval = templateSrv.replace(target.downsampleInterval || interval); + var interval = templateSrv.replace(target.downsampleInterval || options.interval); if (interval.match(/\.[0-9]+s/)) { interval = parseFloat(interval)*1000 + "ms"; @@ -178,20 +254,20 @@ function (angular, _, kbn) { query.tags = angular.copy(target.tags); if(query.tags){ for(var key in query.tags){ - query.tags[key] = templateSrv.replace(query.tags[key]); + query.tags[key] = templateSrv.replace(query.tags[key], options.scopedVars); } } return query; } - function mapMetricsToTargets(metrics, targets) { + function mapMetricsToTargets(metrics, options) { var interpolatedTagValue; return _.map(metrics, function(metricData) { - return _.findIndex(targets, function(target) { + return _.findIndex(options.targets, function(target) { return target.metric === metricData.metric && _.all(target.tags, function(tagV, tagK) { - interpolatedTagValue = templateSrv.replace(tagV); + interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars); return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*"; }); }); diff --git a/public/app/plugins/datasource/opentsdb/partials/query.editor.html b/public/app/plugins/datasource/opentsdb/partials/query.editor.html index a5478ff0cc3..0e456a29b8b 100644 --- a/public/app/plugins/datasource/opentsdb/partials/query.editor.html +++ b/public/app/plugins/datasource/opentsdb/partials/query.editor.html @@ -55,7 +55,7 @@ placeholder="metric name" data-min-length=0 data-items=100 ng-model-onblur - ng-blur="targetBlur()" + ng-change="targetBlur()" > + + + +{{.Name}}, please reset your password + + + + + diff --git a/public/emails/welcome_on_signup.html b/public/emails/welcome_on_signup.html new file mode 100644 index 00000000000..5018930da57 --- /dev/null +++ b/public/emails/welcome_on_signup.html @@ -0,0 +1,30 @@ +{{Subject .Subject "Welcome to Grafana"}} + + + + + +{{.Name}} Welcome to Grafana + + +
        +
        +
        +
        +
        +
        + Hi {{.Name}}, +
        +
        + +
        +
        +
        +
        +
        + © 2014 Grafana +
        +
        +
        + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000000..1f53798bb4f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index 905b8896cd4..7d0be5583cb 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -135,6 +135,11 @@ define([ expect(ctx.rows[1].repeat).to.be(null); }); + it('should add scopedVars to rows', function() { + expect(ctx.rows[0].scopedVars.servers.value).to.be('se1'); + expect(ctx.rows[1].scopedVars.servers.value).to.be('se2'); + }); + it('should generate a repeartRowId based on repeat row index', function() { expect(ctx.rows[1].repeatRowId).to.be(1); }); diff --git a/public/test/specs/graph-specs.js b/public/test/specs/graph-specs.js index a234323ee01..b29076db9a6 100644 --- a/public/test/specs/graph-specs.js +++ b/public/test/specs/graph-specs.js @@ -36,6 +36,7 @@ define([ } }; + scope.panelRenderingComplete = sinon.spy(); scope.appEvent = sinon.spy(); scope.onAppEvent = sinon.spy(); scope.hiddenSeries = {}; diff --git a/public/test/specs/influx09-querybuilder-specs.js b/public/test/specs/influx09-querybuilder-specs.js index 83f897ddb67..b515cfab6fb 100644 --- a/public/test/specs/influx09-querybuilder-specs.js +++ b/public/test/specs/influx09-querybuilder-specs.js @@ -27,10 +27,29 @@ define([ var query = builder.build(); it('should generate correct query', function() { - expect(query).to.be('SELECT mean(value) FROM "cpu" WHERE hostname=\'server1\' AND $timeFilter' + expect(query).to.be('SELECT mean(value) FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter' + ' GROUP BY time($interval) ORDER BY asc'); }); + it('should switch regex operator with tag value is regex', function() { + var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'app', value: '/e.*/'}]}); + var query = builder.build(); + expect(query).to.be('SELECT mean(value) FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($interval) ORDER BY asc'); + }); + }); + + describe('series with multiple fields', function() { + var builder = new InfluxQueryBuilder({ + measurement: 'cpu', + tags: [], + fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }] + }); + + var query = builder.build(); + + it('should generate correct query', function() { + expect(query).to.be('SELECT sum(tx_in), mean(tx_out) FROM "cpu" WHERE $timeFilter GROUP BY time($interval) ORDER BY asc'); + }); }); describe('series with multiple tags only', function() { @@ -42,7 +61,21 @@ define([ var query = builder.build(); it('should generate correct query', function() { - expect(query).to.be('SELECT mean(value) FROM "cpu" WHERE hostname=\'server1\' AND app=\'email\' AND ' + + expect(query).to.be('SELECT mean(value) FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' + + '$timeFilter GROUP BY time($interval) ORDER BY asc'); + }); + }); + + describe('series with tags OR condition', function() { + var builder = new InfluxQueryBuilder({ + measurement: 'cpu', + tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}] + }); + + var query = builder.build(); + + it('should generate correct query', function() { + expect(query).to.be('SELECT mean(value) FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' + '$timeFilter GROUP BY time($interval) ORDER BY asc'); }); }); @@ -57,7 +90,7 @@ define([ var query = builder.build(); expect(query).to.be('SELECT mean(value) FROM "cpu" WHERE $timeFilter ' + - 'GROUP BY time($interval), host ORDER BY asc'); + 'GROUP BY time($interval), "host" ORDER BY asc'); }); }); @@ -78,7 +111,7 @@ define([ it('should have where condition in tag keys query with tags', function() { var builder = new InfluxQueryBuilder({ measurement: '', tags: [{key: 'host', value: 'se1'}] }); var query = builder.buildExploreQuery('TAG_KEYS'); - expect(query).to.be("SHOW TAG KEYS WHERE host='se1'"); + expect(query).to.be("SHOW TAG KEYS WHERE \"host\" = 'se1'"); }); it('should have no conditions in measurement query for query with no tags', function() { @@ -90,7 +123,7 @@ define([ it('should have where condition in measurement query for query with tags', function() { var builder = new InfluxQueryBuilder({measurement: '', tags: [{key: 'app', value: 'email'}]}); var query = builder.buildExploreQuery('MEASUREMENTS'); - expect(query).to.be("SHOW MEASUREMENTS WHERE app='email'"); + expect(query).to.be("SHOW MEASUREMENTS WHERE \"app\" = 'email'"); }); it('should have where tag name IN filter in tag values query for query with one tag', function() { @@ -102,7 +135,19 @@ define([ it('should have measurement tag condition and tag name IN filter in tag values query', function() { var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'app', value: 'email'}, {key: 'host', value: 'server1'}]}); var query = builder.buildExploreQuery('TAG_VALUES', 'app'); - expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE host=\'server1\''); + expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" = \'server1\''); + }); + + it('should switch to regex operator in tag condition', function() { + var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'host', value: '/server.*/'}]}); + var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/'); + }); + + it('should build show field query', function() { + var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'app', value: 'email'}]}); + var query = builder.buildExploreQuery('FIELDS'); + expect(query).to.be('SHOW FIELD KEYS FROM "cpu"'); }); }); diff --git a/public/test/specs/influxSeries-specs.js b/public/test/specs/influxSeries-specs.js index 3a767f23988..fddb873ea35 100644 --- a/public/test/specs/influxSeries-specs.js +++ b/public/test/specs/influxSeries-specs.js @@ -5,19 +5,109 @@ define([ describe('when generating timeseries from influxdb response', function() { + describe('given multiple fields for series', function() { + var options = { series: [ + { + name: 'cpu', + tags: {app: 'test', server: 'server1'}, + columns: ['time', 'mean', 'max', 'min'], + values: [[1431946625000, 10, 11, 9], [1431946626000, 20, 21, 19]] + } + ]}; + describe('and no alias', function() { + it('should generate multiple datapoints for each column', function() { + var series = new InfluxSeries(options); + var result = series.getTimeSeries(); + + expect(result.length).to.be(3); + expect(result[0].target).to.be('cpu.mean {app: test, server: server1}'); + expect(result[0].datapoints[0][0]).to.be(10); + expect(result[0].datapoints[0][1]).to.be(1431946625000); + expect(result[0].datapoints[1][0]).to.be(20); + expect(result[0].datapoints[1][1]).to.be(1431946626000); + + expect(result[1].target).to.be('cpu.max {app: test, server: server1}'); + expect(result[1].datapoints[0][0]).to.be(11); + expect(result[1].datapoints[0][1]).to.be(1431946625000); + expect(result[1].datapoints[1][0]).to.be(21); + expect(result[1].datapoints[1][1]).to.be(1431946626000); + + expect(result[2].target).to.be('cpu.min {app: test, server: server1}'); + expect(result[2].datapoints[0][0]).to.be(9); + expect(result[2].datapoints[0][1]).to.be(1431946625000); + expect(result[2].datapoints[1][0]).to.be(19); + expect(result[2].datapoints[1][1]).to.be(1431946626000); + + }); + }); + + describe('and simple alias', function() { + it('should use alias', function() { + options.alias = 'new series'; + var series = new InfluxSeries(options); + var result = series.getTimeSeries(); + + expect(result[0].target).to.be('new series'); + expect(result[1].target).to.be('new series'); + expect(result[2].target).to.be('new series'); + }); + + }); + + describe('and alias patterns', function() { + it('should replace patterns', function() { + options.alias = 'alias: $m -> $tag_server ([[measurement]])'; + var series = new InfluxSeries(options); + var result = series.getTimeSeries(); + + expect(result[0].target).to.be('alias: cpu -> server1 (cpu)'); + expect(result[1].target).to.be('alias: cpu -> server1 (cpu)'); + expect(result[2].target).to.be('alias: cpu -> server1 (cpu)'); + }); + + }); + }); + describe('given measurement with default fieldname', function() { + var options = { series: [ + { + name: 'cpu', + tags: {app: 'test', server: 'server1'}, + columns: ['time', 'value'], + values: [["2015-05-18T10:57:05Z", 10], ["2015-05-18T10:57:06Z", 12]] + }, + { + name: 'cpu', + tags: {app: 'test2', server: 'server2'}, + columns: ['time', 'value'], + values: [["2015-05-18T10:57:05Z", 15], ["2015-05-18T10:57:06Z", 16]] + } + ]}; + + describe('and no alias', function() { + + it('should generate label with no field', function() { + var series = new InfluxSeries(options); + var result = series.getTimeSeries(); + + expect(result[0].target).to.be('cpu {app: test, server: server1}'); + expect(result[1].target).to.be('cpu {app: test2, server: server2}'); + }); + }); + + }); describe('given two series', function() { var options = { series: [ { name: 'cpu', tags: {app: 'test', server: 'server1'}, columns: ['time', 'mean'], - values: [["2015-05-18T10:57:05Z", 10], ["2015-05-18T10:57:06Z", 12]] + values: [[1431946625000, 10], [1431946626000, 12]] }, { name: 'cpu', tags: {app: 'test2', server: 'server2'}, columns: ['time', 'mean'], - values: [["2015-05-18T10:57:05Z", 15], ["2015-05-18T10:57:06Z", 16]] + values: [[1431946625000, 15], [1431946626000, 16]] } ]}; @@ -28,13 +118,13 @@ define([ var result = series.getTimeSeries(); expect(result.length).to.be(2); - expect(result[0].target).to.be('cpu {app: test, server: server1}'); + expect(result[0].target).to.be('cpu.mean {app: test, server: server1}'); expect(result[0].datapoints[0][0]).to.be(10); expect(result[0].datapoints[0][1]).to.be(1431946625000); expect(result[0].datapoints[1][0]).to.be(12); expect(result[0].datapoints[1][1]).to.be(1431946626000); - expect(result[1].target).to.be('cpu {app: test2, server: server2}'); + expect(result[1].target).to.be('cpu.mean {app: test2, server: server2}'); expect(result[1].datapoints[0][0]).to.be(15); expect(result[1].datapoints[0][1]).to.be(1431946625000); expect(result[1].datapoints[1][0]).to.be(16); diff --git a/public/test/specs/influxdbQueryCtrl-specs.js b/public/test/specs/influxdbQueryCtrl-specs.js index 4ce9b0e6696..564ed6930ba 100644 --- a/public/test/specs/influxdbQueryCtrl-specs.js +++ b/public/test/specs/influxdbQueryCtrl-specs.js @@ -65,6 +65,18 @@ define([ }); }); + describe('when last tag value segment is updated to regex', function() { + beforeEach(function() { + ctx.scope.init(); + ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0); + ctx.scope.tagSegmentUpdated({value: '/server.*/', type: 'value'}, 2); + }); + + it('should update operator', function() { + expect(ctx.scope.tagSegments[1].value).to.be('=~'); + }); + }); + describe('when second tag key is added', function() { beforeEach(function() { ctx.scope.init(); diff --git a/public/test/specs/kairosdb-datasource-specs.js b/public/test/specs/kairosdb-datasource-specs.js new file mode 100644 index 00000000000..43004656573 --- /dev/null +++ b/public/test/specs/kairosdb-datasource-specs.js @@ -0,0 +1,63 @@ +define([ + 'helpers', + 'plugins/datasource/kairosdb/datasource' +], function(helpers) { + 'use strict'; + + describe('KairosDBDatasource', function() { + var ctx = new helpers.ServiceTestContext(); + + beforeEach(module('grafana.services')); + beforeEach(ctx.providePhase(['templateSrv'])); + beforeEach(ctx.createService('KairosDBDatasource')); + beforeEach(function() { + ctx.ds = new ctx.service({ url: ''}); + }); + + describe('When querying kairosdb with one target using query editor target spec', function() { + var results; + var urlExpected = "/api/v1/datapoints/query"; + var bodyExpected = { + metrics: [{ name: "test" }], + cache_time: 0, + start_relative: { + value: "1", + unit: "hours" + } + }; + + var query = { + range: { from: 'now-1h', to: 'now' }, + targets: [{ metric: 'test', downsampling: '(NONE)'}] + }; + + var response = { + queries: [{ + sample_size: 60, + results: [{ + name: "test", + values: [[1420070400000, 1]] + }] + }] + }; + + beforeEach(function() { + ctx.$httpBackend.expect('POST', urlExpected, bodyExpected).respond(response); + ctx.ds.query(query).then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + }); + + it('should generate the correct query', function() { + ctx.$httpBackend.verifyNoOutstandingExpectation(); + }); + + it('should return series list', function() { + expect(results.data.length).to.be(1); + expect(results.data[0].target).to.be('test'); + }); + + }); + + }); + +}); diff --git a/public/test/specs/kbn-format-specs.js b/public/test/specs/kbn-format-specs.js index d8a11c5a43c..1a5f60975c0 100644 --- a/public/test/specs/kbn-format-specs.js +++ b/public/test/specs/kbn-format-specs.js @@ -51,7 +51,12 @@ define([ }); }); - + describe('kbn roundValue', function() { + it('should should handle null value', function() { + var str = kbn.roundValue(null, 2); + expect(str).to.be(null); + }); + }); describe('calculateInterval', function() { it('1h 100 resultion', function() { diff --git a/public/test/specs/opentsdbDatasource-specs.js b/public/test/specs/opentsdbDatasource-specs.js new file mode 100644 index 00000000000..ace7e21292a --- /dev/null +++ b/public/test/specs/opentsdbDatasource-specs.js @@ -0,0 +1,52 @@ +define([ + 'helpers', + 'plugins/datasource/opentsdb/datasource' +], function(helpers) { + 'use strict'; + + describe('opentsdb', function() { + var ctx = new helpers.ServiceTestContext(); + + beforeEach(module('grafana.services')); + beforeEach(ctx.providePhase(['backendSrv'])); + + beforeEach(ctx.createService('OpenTSDBDatasource')); + beforeEach(function() { + ctx.ds = new ctx.service({ url: [''] }); + }); + + describe('When performing metricFindQuery', function() { + var results; + var requestOptions; + + beforeEach(function() { + ctx.backendSrv.datasourceRequest = function(options) { + requestOptions = options; + return ctx.$q.when({data: [{ target: 'prod1.count', datapoints: [[10, 1], [12,1]] }]}); + }; + }); + + it('metrics() should generate api suggest query', function() { + ctx.ds.metricFindQuery('metrics()').then(function(data) { results = data; }); + ctx.$rootScope.$apply(); + expect(requestOptions.url).to.be('/api/suggest'); + }); + + it('tag_names(cpu) should generate looku query', function() { + ctx.ds.metricFindQuery('tag_names(cpu)').then(function(data) { results = data; }); + ctx.$rootScope.$apply(); + expect(requestOptions.url).to.be('/api/search/lookup'); + expect(requestOptions.params.m).to.be('cpu'); + }); + + it('tag_values(cpu, test) should generate looku query', function() { + ctx.ds.metricFindQuery('tag_values(cpu, hostname)').then(function(data) { results = data; }); + ctx.$rootScope.$apply(); + expect(requestOptions.url).to.be('/api/search/lookup'); + expect(requestOptions.params.m).to.be('cpu{hostname=*}'); + }); + + }); + }); +}); + diff --git a/public/test/specs/parser-specs.js b/public/test/specs/parser-specs.js index 9fead11a9c3..8f5dd1b37ef 100644 --- a/public/test/specs/parser-specs.js +++ b/public/test/specs/parser-specs.js @@ -118,11 +118,11 @@ define([ expect(rootNode.pos).to.be(19); }); - it('invalid function expression missing closing paranthesis', function() { + it('invalid function expression missing closing parenthesis', function() { var parser = new Parser('sum(test'); var rootNode = parser.getAst(); - expect(rootNode.message).to.be('Expected closing paranthesis instead found end of string'); + expect(rootNode.message).to.be('Expected closing parenthesis instead found end of string'); expect(rootNode.pos).to.be(9); }); diff --git a/public/test/specs/templateValuesSrv-specs.js b/public/test/specs/templateValuesSrv-specs.js index 2ff287b8b6d..6c7a3035ff9 100644 --- a/public/test/specs/templateValuesSrv-specs.js +++ b/public/test/specs/templateValuesSrv-specs.js @@ -68,7 +68,7 @@ define([ expect(variable.current.value.length).to.be(2); expect(variable.current.value[0]).to.be("new"); expect(variable.current.value[1]).to.be("other"); - expect(variable.current.text).to.be("new, other"); + expect(variable.current.text).to.be("new + other"); }); }); @@ -106,6 +106,31 @@ define([ }); }); + describeUpdateVariable('query variable with empty current object and refresh', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: '', name: 'test', current: {} }; + scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}]; + }); + + it('should set current value to first option', function() { + expect(scenario.variable.options.length).to.be(2); + expect(scenario.variable.current.value).to.be('backend1'); + }); + }); + + describeUpdateVariable('interval variable without auto', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' }; + }); + + it('should update options array', function() { + expect(scenario.variable.options.length).to.be(4); + expect(scenario.variable.options[0].text).to.be('1s'); + expect(scenario.variable.options[0].value).to.be('1s'); + }); + }); + + describeUpdateVariable('interval variable with auto', function(scenario) { scenario.setup(function() { scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 }; @@ -171,7 +196,7 @@ define([ describeUpdateVariable('and existing value still exists in options', function(scenario) { scenario.setup(function() { scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; - scenario.variable.current = { text: 'backend2'}; + scenario.variable.current = { value: 'backend2', text: 'backend2'}; scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}]; }); @@ -199,8 +224,9 @@ define([ scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}]; }); - it('should not add non matching items', function() { - expect(scenario.variable.options.length).to.be(0); + it('should not add non matching items, None option should be added instead', function() { + expect(scenario.variable.options.length).to.be(1); + expect(scenario.variable.options[0].isNone).to.be(true); }); }); diff --git a/public/test/specs/selectDropdownCtrl-specs.js b/public/test/specs/valueSelectDropdown-specs.js similarity index 83% rename from public/test/specs/selectDropdownCtrl-specs.js rename to public/test/specs/valueSelectDropdown-specs.js index 3aae3429a7c..4a7a868d643 100644 --- a/public/test/specs/selectDropdownCtrl-specs.js +++ b/public/test/specs/valueSelectDropdown-specs.js @@ -1,5 +1,5 @@ define([ - 'directives/variableValueSelect', + 'directives/valueSelectDropdown', ], function () { 'use strict'; @@ -15,7 +15,7 @@ function () { beforeEach(inject(function($controller, $rootScope, $q) { rootScope = $rootScope; scope = $rootScope.$new(); - ctrl = $controller('SelectDropdownCtrl', {$scope: scope}); + ctrl = $controller('ValueSelectDropdownCtrl', {$scope: scope}); ctrl.getValuesForTag = function(obj) { return $q.when(tagValuesMap[obj.tagKey]); }; @@ -38,7 +38,7 @@ function () { ctrl.variable = { current: {text: 'server-1', value: 'server-1'}, options: [ - {text: 'server-1', value: 'server-1'}, + {text: 'server-1', value: 'server-1', selected: true}, {text: 'server-2', value: 'server-2'}, {text: 'server-3', value: 'server-3'}, ], @@ -134,5 +134,28 @@ function () { }); }); }); + + describe("Given variable with selected tags", function() { + beforeEach(function() { + ctrl.variable = { + current: {text: 'server-1', value: 'server-1', tags: [{text: 'key1', selected: true}] }, + options: [ + {text: 'server-1', value: 'server-1'}, + {text: 'server-2', value: 'server-2'}, + {text: 'server-3', value: 'server-3'}, + ], + tags: ["key1", "key2", "key3"], + multi: true + }; + ctrl.init(); + ctrl.show(); + }); + + it("should set tag as selected", function() { + expect(ctrl.tags[0].selected).to.be(true); + }); + + }); + }); }); diff --git a/public/test/test-main.js b/public/test/test-main.js index 6b62df02102..e6e18512216 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -17,34 +17,34 @@ require.config({ chromath: '../vendor/chromath', filesaver: '../vendor/filesaver', - angular: '../vendor/angular/angular', - 'angular-route': '../vendor/angular/angular-route', - 'angular-sanitize': '../vendor/angular/angular-sanitize', - angularMocks: '../vendor/angular/angular-mocks', - 'angular-dragdrop': '../vendor/angular/angular-dragdrop', - 'angular-strap': '../vendor/angular/angular-strap', - timepicker: '../vendor/angular/timepicker', - datepicker: '../vendor/angular/datepicker', - bindonce: '../vendor/angular/bindonce', + angular: '../vendor/angular/angular', + 'angular-route': '../vendor/angular-route/angular-route', + 'angular-sanitize': '../vendor/angular-sanitize/angular-sanitize', + angularMocks: '../vendor/angular-mocks/angular-mocks', + 'angular-dragdrop': '../vendor/angular-native-dragdrop/draganddrop', + 'angular-strap': '../vendor/angular-other/angular-strap', + timepicker: '../vendor/angular-other/timepicker', + datepicker: '../vendor/angular-other/datepicker', + bindonce: '../vendor/angular-bindonce/bindonce', crypto: '../vendor/crypto.min', spectrum: '../vendor/spectrum', - jquery: '../vendor/jquery/jquery-2.1.3', + jquery: '../vendor/jquery/dist/jquery', bootstrap: '../vendor/bootstrap/bootstrap', 'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput', 'extend-jquery': 'components/extend-jquery', - 'jquery.flot': '../vendor/jquery/jquery.flot', - 'jquery.flot.pie': '../vendor/jquery/jquery.flot.pie', - 'jquery.flot.events': '../vendor/jquery/jquery.flot.events', - 'jquery.flot.selection': '../vendor/jquery/jquery.flot.selection', - 'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack', - 'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent', - 'jquery.flot.time': '../vendor/jquery/jquery.flot.time', - 'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair', - 'jquery.flot.fillbelow': '../vendor/jquery/jquery.flot.fillbelow', + 'jquery.flot': '../vendor/flot/jquery.flot', + 'jquery.flot.pie': '../vendor/flot/jquery.flot.pie', + 'jquery.flot.events': '../vendor/flot/jquery.flot.events', + 'jquery.flot.selection': '../vendor/flot/jquery.flot.selection', + 'jquery.flot.stack': '../vendor/flot/jquery.flot.stack', + 'jquery.flot.stackpercent':'../vendor/flot/jquery.flot.stackpercent', + 'jquery.flot.time': '../vendor/flot/jquery.flot.time', + 'jquery.flot.crosshair': '../vendor/flot/jquery.flot.crosshair', + 'jquery.flot.fillbelow': '../vendor/flot/jquery.flot.fillbelow', modernizr: '../vendor/modernizr-2.6.1', }, @@ -130,6 +130,7 @@ require([ 'specs/influx09-querybuilder-specs', 'specs/influxdb-datasource-specs', 'specs/influxdbQueryCtrl-specs', + 'specs/kairosdb-datasource-specs', 'specs/graph-ctrl-specs', 'specs/graph-specs', 'specs/graph-tooltip-specs', @@ -144,7 +145,8 @@ require([ 'specs/singlestat-specs', 'specs/dynamicDashboardSrv-specs', 'specs/unsavedChangesSrv-specs', - 'specs/selectDropdownCtrl-specs', + 'specs/valueSelectDropdown-specs', + 'specs/opentsdbDatasource-specs', ]; var pluginSpecs = (config.plugins.specs || []).map(function (spec) { @@ -155,4 +157,3 @@ require([ window.__karma__.start(); }); }); - diff --git a/public/vendor/angular-bindonce/.bower.json b/public/vendor/angular-bindonce/.bower.json new file mode 100644 index 00000000000..313c654cc14 --- /dev/null +++ b/public/vendor/angular-bindonce/.bower.json @@ -0,0 +1,37 @@ +{ + "name": "angular-bindonce", + "version": "0.3.3", + "main": "bindonce.js", + "description": "Zero watchers binding directives for AngularJS", + "homepage": "https://github.com/Pasvaz/bindonce", + "author": "Pasquale Vazzana ", + "repository": { + "type": "git", + "url": "https://github.com/Pasvaz/bindonce.git" + }, + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "components" + ], + "dependencies": {}, + "keywords": [ + "angularjs", + "angular", + "directive", + "binding", + "watcher", + "bindonce" + ], + "_release": "0.3.3", + "_resolution": { + "type": "version", + "tag": "0.3.3", + "commit": "0fcf71e6effc88179893c9c06baf6c6bf9037632" + }, + "_source": "git://github.com/Pasvaz/bindonce.git", + "_target": "~0.3.3", + "_originalSource": "angular-bindonce", + "_direct": true +} \ No newline at end of file diff --git a/public/vendor/angular-bindonce/CHANGELOG.md b/public/vendor/angular-bindonce/CHANGELOG.md new file mode 100644 index 00000000000..8246d986e39 --- /dev/null +++ b/public/vendor/angular-bindonce/CHANGELOG.md @@ -0,0 +1,43 @@ +# 0.3.3 (2014-02-12) +### Features +- **bo-disabled:** + - Add support for ng-disabled/bo-disabled #110 + +
        +# 0.3.2 (2014-11-23) +### Bug Fixes +- **Angular 1.3 compatibility** + +
        +# 0.3.1 (2014-02-12) +### Features +- **bo-bind:** + - alias for bo-text + +### Bug Fixes +- **Angular Promises** + - Ensures that promises are resolved before to run binders ([b3ef1b4](https://github.com/Pasvaz/bindonce/commit/b3ef1b46edfe83f10ed455d5520027f731563f32)) + +### Minor improvements +- Updated Readme + +
        +# 0.3.0 (2014-01-21) +### Features +- **bo-switch:** + - Create new directive: bo-switch ([652d0db](https://github.com/Pasvaz/bindonce/commit/652d0db04325166a180377c738a376543b5f2357)) + +
        +# 0.2.3 (2014-01-20) +### Bug Fixes + +- **bo-if:** + - Ensures that we both process newly added binders from bo-if, and that +we only process each binder once ([d11f863](https://github.com/Pasvaz/bindonce/commit/e091c273bbd17603d410fecc363874f0d1e6f38e)) + +### Features + +- **Minification:** + - add min file ([47277ee](https://github.com/Pasvaz/bindonce/commit/47277eedd092b3210de362c725a7dadcddac8e87)) +- **Changelog:** + - Created a changelog file diff --git a/public/vendor/angular-bindonce/README.md b/public/vendor/angular-bindonce/README.md new file mode 100644 index 00000000000..c8d753fbe6b --- /dev/null +++ b/public/vendor/angular-bindonce/README.md @@ -0,0 +1,154 @@ +Bindonce +======== + +High performance binding for AngularJs + +## Usage +* download, clone or fork it or install it using [bower](http://twitter.github.com/bower/) `bower install angular-bindonce` +* Include the `bindonce.js` script provided by this component into your app. +* Add `'pasvaz.bindonce'` as a module dependency to your app: `angular.module('app', ['pasvaz.bindonce'])` + +## Demo +Here is an example of how AngularJs can [freeze your UI](http://plnkr.co/edit/jwrHVb?p=preview), try to press and hold a key inside the input field, when the table is filled with only 1 person everything is ok, you can see how the DOM is updated by the input in real time, however if you try to load 1000 person *(or even 500 if the testing device is not powerfull)* and repeat the experiment you can see how the UI is frozen. In [this other demo](http://plnkr.co/edit/0DGOrk?p=preview) BindOnce will take care of your watchers and the UI will be reactive as it should be. The code is the same for both demos, the only difference is that I replaced any `ng-*` tag inside the table with the equivalent `bo-*` tag. +* [AngularJs regular Demo](http://plnkr.co/edit/jwrHVb?p=preview) +* [Demo with Bindonce](http://plnkr.co/edit/0DGOrk?p=preview) + +## Overview +AngularJs provides a great data binding system but if you abuse of it the page can run into some performance issues, it's known that more of 2000 watchers can lag the UI and that amount can be reached easily if you don't pay attention to the data-binding. Sometime you really need to bind your data using watchers, especially for SPA because the data are updated in real time, but often you can avoid it with some efforts, most of the data presented in your page, once rendered, are immutable so you shouldn't keep watching them for changes. + +For instance, take a look to this snippet: +```html +
          +
        • + + +

          +
        • +
        +``` +Angular internally creates a `$watch` for each `ng-*` directive in order to keep the data up to date, so in this example just for displaying few info it creates 6 + 1 *(ngRepeatWatch)* watchers per `person`, even if the `person` is supposed to remain the same once shown. Iterate this amount for each person and you can have an idea about how easy is to reach 2000 watchers. Now if you need it because those data could change while you show the page or are bound to some models, it's ok. But most of the time they are static data that don't change once rendered. This is where **bindonce** can really help you. + +The above example done with **bindonce**: +```html +
          +
        • + + +

          +
        • +
        +``` +Now this example uses **0 watches** per `person` and renders exactly the same result as the above that uses ng-*. *(Angular still uses 1 watcher for ngRepeatWatch)* + +### The smart approach +OK until here nothing completely new, with a bit of efforts you could create your own directive and render the `person` inside the `link` function, or you could use [watch fighters](https://github.com/abourget/abourget-angular) that has a similar approach, but there is still one problem that you have to face and **bindonce** already handles it: *the existence of the data when the directive renders the content*. Usually the directives, unless you use watchers or bind their attributes to the scope (still a watcher), render the content when they are loaded into the markup, but if at that given time your data is not available, the directive can't render it. Bindonce can wait until the data is ready before to rendering the content. +Let's take a look at the follow snippet to better understand the concept: +```html + + +... + +``` +This basic directive works as expected, it renders the `Person` data without using any watchers. However, if `Person` is not yet available inside the $scope when the page is loaded (say we get `Person` via $http or via $resource), the directive is useless, `scope.$eval(attr.myCustomSetText)` simply renders nothing and exits. + +Here is how we can solve this issue with **bindonce**: +```html +
        + + + +

        +
        +``` +`bindonce="Person"` does the trick, any `bo-*` attribute belonging to `bindonce` waits until the parent `bindonce="{somedata}"` is validated and then renders its content. Once the scope contains the value `Person` then each bo-* child gets filled with the proper values. In order to accomplish this task, **bindonce** uses just **one** temporary watcher, no matters how many children need to be rendered. As soon as it gets `Person` the watcher is promptly removed. If the $scope already contains the data `bindonce` is looking for, then it doesn't create the temporary watcher and simply starts rendering its children. + +You may have noticed that the first example didn't assign any value to the `bindonce` attribute: +```html +
          +
        • + ... +``` +when used with `ng-repeat` `bindonce` doesn't need to check if `person` is defined because `ng-repeat` creates the directives only when `person` exists. You could be more explicit: `
        • `, however assigning a value to `bindonce` in an `ng-repeat` won't make any difference. + +### Interpolation +Some directives (ng-href, ng-src) use interpolation, ie: `ng-href="/profile/{{User.profileId}}"`. +Both `ng-href` and `ng-src` have the bo-* equivalent directives: `bo-href-i` and `bo-src-i` (pay attention to the **-i**, it stands for **interpolate**). As expected they don't use watchers however Angular creates one watcher per interpolation, for instance `bo-href-i="/profile/{{User.profileId}}"` sets the element's href **once**, as expected, but Angular keeps a watcher active on `{{User.profileId}}` even if `bo-href-i` doesn't use it. +That's why by default the `bo-href` doesn't use interpolation or watchers. The above equivalent with 0 watchers would be `bo-href="'/profile/' + User.profileId"`. Nevertheless, `bo-href-i` and `bo-src-i` are still maintained for compatibility reasons. + +### Filters +Almost every `bo-*` directive replace the equivalent `ng-*` and works in the same ways, except it is evaluated once. +Consequentially you can use any valid angular expression, including filters. This is an example how to use a filter: +```html +
          + +
          +``` + +## Attribute Usage +| Directive | Description | Example | +|------------|----------------|-----| +| `bindonce="{somedata}"`| **bindonce** is the main directive. `{somedata}` is optional, and if present, forces bindonce to wait until `somedata` is defined before rendering its children | `
          ...
          ` | +| `bo-if = "condition"` | equivalent to `ng-if` but doesn't use watchers |``| +| `bo-switch = "expression"` | equivalent to `ng-switch` but doesn't use watchers |`
          ` `public` `private` `
          `| +| `bo-show = "condition"` | equivalent to `ng-show` but doesn't use watchers |``| +| `bo-hide = "condition"` | equivalent to `ng-hide` but doesn't use watchers |``| +| `bo-disabled = "condition"` | equivalent to `ng-disabled` but doesn't use watchers |``| +| `bo-text = "text"` | evaluates "text" and print it as text inside the element | `` | +| `bo-bind = "text"` | alias for `bo-text`, equivalent to `ng-bind` but doesn't use watchers | `` | +| `bo-html = "markup"` | evaluates "markup" and render it as html inside the element |`bo-html="Person.description"`| +| `bo-href-i = "url"`
          *use `bo-href` instead* | **equivalent** to `ng-href`.
          **Heads up!** Using interpolation `{{}}` it creates one watcher:
          `bo-href-i="/p/{{Person.id}}"`.
          Use `bo-href` to avoid the watcher:
          `bo-href="'/p/' + Person.id"` |``| +| `bo-href = "url"` | **similar** to `ng-href` but doesn't allow interpolation using `{{}}` like `ng-href`.
          **Heads up!** You can't use interpolation `{{}}` inside the url, use bo-href-i for that purpose |``
          or
          ``| +| `bo-src-i = "url"`
          *use `bo-src` instead* | **equivalent** to `ng-src`.
          **Heads up!** It creates one watcher |``| +| `bo-src = "url"` | **similar** to `ng-src` but doesn't allow interpolation using `{{}}` like `ng-src`.
          **Heads up!** You can't use interpolation `{{}}`, use bo-src-i for that purpose |``| +| `bo-class = "object/string"` | equivalent to `ng-class` but doesn't use watchers |``| +| `bo-alt = "text"` | evaluates "text" and render it as `alt` for the element |``| +| `bo-title = "text"` | evaluates "text" and render it as `title` for the element |``| +| `bo-id = "#id"` | evaluates "#id" and render it as `id` for the element |``| +| `bo-style = "object"` | equivalent to `ng-style` but doesn't use watchers |``| +| `bo-value = "expression"` | evaluates "expression" and render it as `value` for the element |``| +| `bo-attr bo-attr-foo = "text"` | evaluates "text" and render it as a custom attribute for the element |`
          `| + +## Build +``` +$ npm install uglify-js -g +$ uglifyjs bindonce.js -c -m -o bindonce.min.js +``` + +## Todo +Tests + +## Copyright +BindOnce was written by **Pasquale Vazzana**, you can follow him on [google+](https://plus.google.com/101872882413388363602) or on [@twitter](https://twitter.com/PasqualeVazzana) + +Thanks to all the [contributors](https://github.com/Pasvaz/bindonce/graphs/contributors) + +## LICENSE - "MIT License" + +Copyright (c) 2013-2014 Pasquale Vazzana + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/vendor/angular/bindonce.js b/public/vendor/angular-bindonce/bindonce.js similarity index 98% rename from public/vendor/angular/bindonce.js rename to public/vendor/angular-bindonce/bindonce.js index 77bd81e7598..8742d891750 100644 --- a/public/vendor/angular/bindonce.js +++ b/public/vendor/angular-bindonce/bindonce.js @@ -2,7 +2,7 @@ "use strict"; /** * Bindonce - Zero watches binding for AngularJs - * @version v0.3.2 + * @version v0.3.3 * @link https://github.com/Pasvaz/bindonce * @author Pasquale Vazzana * @license MIT License, http://www.opensource.org/licenses/MIT @@ -185,6 +185,9 @@ case 'style': binder.element.css(value); break; + case 'disabled': + binder.element.prop('disabled', value); + break; case 'src': binder.element.attr(binder.attr, value); if (msie) binder.element.prop('src', value); @@ -251,6 +254,7 @@ { directiveName: 'boTitle', attribute: 'title' }, { directiveName: 'boId', attribute: 'id' }, { directiveName: 'boStyle', attribute: 'style' }, + { directiveName: 'boDisabled', attribute: 'disabled' }, { directiveName: 'boValue', attribute: 'value' }, { directiveName: 'boAttr', attribute: 'attr' }, diff --git a/public/vendor/angular-bindonce/bindonce.min.js b/public/vendor/angular-bindonce/bindonce.min.js new file mode 100644 index 00000000000..555ded4bffe --- /dev/null +++ b/public/vendor/angular-bindonce/bindonce.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=angular.module("pasvaz.bindonce",[]);e.directive("bindonce",function(){var e=function(e){if(e&&0!==e.length){var t=angular.lowercase(""+e);e=!("f"===t||"0"===t||"false"===t||"no"===t||"n"===t||"[]"===t)}else e=!1;return e},t=parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10);isNaN(t)&&(t=parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10));var r={restrict:"AM",controller:["$scope","$element","$attrs","$interpolate",function(r,a,i,n){var c=function(t,r,a){var i="show"===r?"":"none",n="hide"===r?"":"none";t.css("display",e(a)?i:n)},o=function(e,t){if(angular.isObject(t)&&!angular.isArray(t)){var r=[];angular.forEach(t,function(e,t){e&&r.push(t)}),t=r}t&&e.addClass(angular.isArray(t)?t.join(" "):t)},s=function(e,t){e.transclude(t,function(t){var r=e.element.parent(),a=e.element&&e.element[e.element.length-1],i=r&&r[0]||a&&a.parentNode,n=a&&a.nextSibling||null;angular.forEach(t,function(e){i.insertBefore(e,n)})})},l={watcherRemover:void 0,binders:[],group:i.boName,element:a,ran:!1,addBinder:function(e){this.binders.push(e),this.ran&&this.runBinders()},setupWatcher:function(e){var t=this;this.watcherRemover=r.$watch(e,function(e){void 0!==e&&(t.removeWatcher(),t.checkBindonce(e))},!0)},checkBindonce:function(e){var t=this,r=e.$promise?e.$promise.then:e.then;"function"==typeof r?r(function(){t.runBinders()}):t.runBinders()},removeWatcher:function(){void 0!==this.watcherRemover&&(this.watcherRemover(),this.watcherRemover=void 0)},runBinders:function(){for(;this.binders.length>0;){var r=this.binders.shift();if(!this.group||this.group==r.group){var a=r.scope.$eval(r.interpolate?n(r.value):r.value);switch(r.attr){case"boIf":e(a)&&s(r,r.scope.$new());break;case"boSwitch":var i,l=r.controller[0];(i=l.cases["!"+a]||l.cases["?"])&&(r.scope.$eval(r.attrs.change),angular.forEach(i,function(e){s(e,r.scope.$new())}));break;case"boSwitchWhen":var u=r.controller[0];u.cases["!"+r.attrs.boSwitchWhen]=u.cases["!"+r.attrs.boSwitchWhen]||[],u.cases["!"+r.attrs.boSwitchWhen].push({transclude:r.transclude,element:r.element});break;case"boSwitchDefault":var u=r.controller[0];u.cases["?"]=u.cases["?"]||[],u.cases["?"].push({transclude:r.transclude,element:r.element});break;case"hide":case"show":c(r.element,r.attr,a);break;case"class":o(r.element,a);break;case"text":r.element.text(a);break;case"html":r.element.html(a);break;case"style":r.element.css(a);break;case"disabled":r.element.prop("disabled",a);break;case"src":r.element.attr(r.attr,a),t&&r.element.prop("src",a);break;case"attr":angular.forEach(r.attrs,function(e,t){var a,i;t.match(/^boAttr./)&&r.attrs[t]&&(a=t.replace(/^boAttr/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=r.scope.$eval(r.attrs[t]),r.element.attr(a,i))});break;case"href":case"alt":case"title":case"id":case"value":r.element.attr(r.attr,a)}}}this.ran=!0}};angular.extend(this,l)}],link:function(e,t,r,a){var i=r.bindonce&&e.$eval(r.bindonce);void 0!==i?a.checkBindonce(i):(a.setupWatcher(r.bindonce),t.bind("$destroy",a.removeWatcher))}};return r}),angular.forEach([{directiveName:"boShow",attribute:"show"},{directiveName:"boHide",attribute:"hide"},{directiveName:"boClass",attribute:"class"},{directiveName:"boText",attribute:"text"},{directiveName:"boBind",attribute:"text"},{directiveName:"boHtml",attribute:"html"},{directiveName:"boSrcI",attribute:"src",interpolate:!0},{directiveName:"boSrc",attribute:"src"},{directiveName:"boHrefI",attribute:"href",interpolate:!0},{directiveName:"boHref",attribute:"href"},{directiveName:"boAlt",attribute:"alt"},{directiveName:"boTitle",attribute:"title"},{directiveName:"boId",attribute:"id"},{directiveName:"boStyle",attribute:"style"},{directiveName:"boDisabled",attribute:"disabled"},{directiveName:"boValue",attribute:"value"},{directiveName:"boAttr",attribute:"attr"},{directiveName:"boIf",transclude:"element",terminal:!0,priority:1e3},{directiveName:"boSwitch",require:"boSwitch",controller:function(){this.cases={}}},{directiveName:"boSwitchWhen",transclude:"element",priority:800,require:"^boSwitch"},{directiveName:"boSwitchDefault",transclude:"element",priority:800,require:"^boSwitch"}],function(t){var r=200;return e.directive(t.directiveName,function(){var e={priority:t.priority||r,transclude:t.transclude||!1,terminal:t.terminal||!1,require:["^bindonce"].concat(t.require||[]),controller:t.controller,compile:function(e,r,a){return function(e,r,i,n){var c=n[0],o=i.boParent;if(o&&c.group!==o){var s=c.element.parent();c=void 0;for(var l;9!==s[0].nodeType&&s.length;){if((l=s.data("$bindonceController"))&&l.group===o){c=l;break}s=s.parent()}if(!c)throw new Error("No bindonce controller: "+o)}c.addBinder({element:r,attr:t.attribute||t.directiveName,attrs:i,value:i[t.directiveName],interpolate:t.interpolate,group:o,transclude:a,controller:n.slice(1),scope:e})}}};return e})})}(); \ No newline at end of file diff --git a/public/vendor/angular-bindonce/bower.json b/public/vendor/angular-bindonce/bower.json new file mode 100644 index 00000000000..9242594598a --- /dev/null +++ b/public/vendor/angular-bindonce/bower.json @@ -0,0 +1,28 @@ +{ + "name": "angular-bindonce", + "version": "0.3.3", + "main": "bindonce.js", + "description": "Zero watchers binding directives for AngularJS", + "homepage": "https://github.com/Pasvaz/bindonce", + "author": "Pasquale Vazzana ", + "repository": { + "type": "git", + "url": "https://github.com/Pasvaz/bindonce.git" + }, + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "components" + ], + "dependencies": { + }, + "keywords": [ + "angularjs", + "angular", + "directive", + "binding", + "watcher", + "bindonce" + ] +} diff --git a/public/vendor/angular-bindonce/package.json b/public/vendor/angular-bindonce/package.json new file mode 100644 index 00000000000..9242594598a --- /dev/null +++ b/public/vendor/angular-bindonce/package.json @@ -0,0 +1,28 @@ +{ + "name": "angular-bindonce", + "version": "0.3.3", + "main": "bindonce.js", + "description": "Zero watchers binding directives for AngularJS", + "homepage": "https://github.com/Pasvaz/bindonce", + "author": "Pasquale Vazzana ", + "repository": { + "type": "git", + "url": "https://github.com/Pasvaz/bindonce.git" + }, + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "components" + ], + "dependencies": { + }, + "keywords": [ + "angularjs", + "angular", + "directive", + "binding", + "watcher", + "bindonce" + ] +} diff --git a/public/vendor/angular-mocks/.bower.json b/public/vendor/angular-mocks/.bower.json new file mode 100644 index 00000000000..898159e6d8b --- /dev/null +++ b/public/vendor/angular-mocks/.bower.json @@ -0,0 +1,20 @@ +{ + "name": "angular-mocks", + "version": "1.4.0", + "main": "./angular-mocks.js", + "ignore": [], + "dependencies": { + "angular": "1.4.0" + }, + "homepage": "https://github.com/angular/bower-angular-mocks", + "_release": "1.4.0", + "_resolution": { + "type": "version", + "tag": "v1.4.0", + "commit": "5a7f9f0bad5da4314df7f638fcaf330ff864cae2" + }, + "_source": "git://github.com/angular/bower-angular-mocks.git", + "_target": "~1.4.0", + "_originalSource": "angular-mocks", + "_direct": true +} \ No newline at end of file diff --git a/public/vendor/angular-mocks/README.md b/public/vendor/angular-mocks/README.md new file mode 100644 index 00000000000..440cce9b78a --- /dev/null +++ b/public/vendor/angular-mocks/README.md @@ -0,0 +1,63 @@ +# packaged angular-mocks + +This repo is for distribution on `npm` and `bower`. The source for this module is in the +[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngMock). +Please file issues and pull requests against that repo. + +## Install + +You can install this package either with `npm` or with `bower`. + +### npm + +```shell +npm install angular-mocks +``` + +You can `require` ngMock modules: + +```js +var angular = require('angular'); +angular.module('myMod', [ + require('angular-animate'), + require('angular-mocks/ngMock') + require('angular-mocks/ngAnimateMock') +]); +``` + +### bower + +```shell +bower install angular-mocks +``` + +The mocks are then available at `bower_components/angular-mocks/angular-mocks.js`. + +## Documentation + +Documentation is available on the +[AngularJS docs site](https://docs.angularjs.org/guide/unit-testing). + +## License + +The MIT License + +Copyright (c) 2010-2015 Google, Inc. http://angularjs.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/vendor/angular/angular-mocks.js b/public/vendor/angular-mocks/angular-mocks.js similarity index 91% rename from public/vendor/angular/angular-mocks.js rename to public/vendor/angular-mocks/angular-mocks.js index 80219656eb6..5df76fe3945 100644 --- a/public/vendor/angular/angular-mocks.js +++ b/public/vendor/angular-mocks/angular-mocks.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.4 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.0 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) { @@ -64,10 +64,9 @@ angular.mock.$Browser = function() { return listener; }; + self.$$applicationDestroyed = angular.noop; self.$$checkUrlChange = angular.noop; - self.cookieHash = {}; - self.lastCookieHash = {}; self.deferredFns = []; self.deferredNextId = 0; @@ -147,11 +146,6 @@ angular.mock.$Browser.prototype = { }); }, - addPollFn: function(pollFn) { - this.pollFns.push(pollFn); - return pollFn; - }, - url: function(url, replace, state) { if (angular.isUndefined(state)) { state = null; @@ -170,25 +164,6 @@ angular.mock.$Browser.prototype = { return this.$$state; }, - cookies: function(name, value) { - if (name) { - if (angular.isUndefined(value)) { - delete this.cookieHash[name]; - } else { - if (angular.isString(value) && //strings only - value.length <= 4096) { //strict cookie storage limits - this.cookieHash[name] = value; - } - } - } else { - if (!angular.equals(this.cookieHash, this.lastCookieHash)) { - this.lastCookieHash = angular.copy(this.cookieHash); - this.cookieHash = angular.copy(this.cookieHash); - } - return this.cookieHash; - } - }, - notifyWhenNoOutstandingRequests: function(fn) { fn(); } @@ -250,31 +225,31 @@ angular.mock.$ExceptionHandlerProvider = function() { * * @param {string} mode Mode of operation, defaults to `rethrow`. * - * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there - * is a bug in the application or test, so this mock will make these tests fail. * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` * mode stores an array of errors in `$exceptionHandler.errors`, to allow later * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and * {@link ngMock.$log#reset reset()} + * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there + * is a bug in the application or test, so this mock will make these tests fail. + * For any implementations that expect exceptions to be thrown, the `rethrow` mode + * will also maintain a log of thrown errors. */ this.mode = function(mode) { - switch (mode) { - case 'rethrow': - handler = function(e) { - throw e; - }; - break; - case 'log': - var errors = []; + switch (mode) { + case 'log': + case 'rethrow': + var errors = []; handler = function(e) { if (arguments.length == 1) { errors.push(e); } else { errors.push([].slice.call(arguments, 0)); } + if (mode === "rethrow") { + throw e; + } }; - handler.errors = errors; break; default: @@ -458,6 +433,7 @@ angular.mock.$LogProvider = function() { * indefinitely. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. * @returns {promise} A promise which will be notified on each iteration. */ angular.mock.$IntervalProvider = function() { @@ -468,13 +444,17 @@ angular.mock.$IntervalProvider = function() { now = 0; var $interval = function(fn, delay, count, invokeApply) { - var iteration = 0, + var hasParams = arguments.length > 4, + args = hasParams ? Array.prototype.slice.call(arguments, 4) : [], + iteration = 0, skipApply = (angular.isDefined(invokeApply) && !invokeApply), deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise; count = (angular.isDefined(count)) ? count : 0; - promise.then(null, null, fn); + promise.then(null, null, (!hasParams) ? fn : function() { + fn.apply(null, args); + }); promise.$$intervalId = nextRepeatId; @@ -581,20 +561,20 @@ function jsonStringToDate(string) { tzHour = 0, tzMin = 0; if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4] || 0) - tzHour, - int(match[5] || 0) - tzMin, - int(match[6] || 0), - int(match[7] || 0)); + date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + date.setUTCHours(toInt(match[4] || 0) - tzHour, + toInt(match[5] || 0) - tzMin, + toInt(match[6] || 0), + toInt(match[7] || 0)); return date; } return string; } -function int(str) { +function toInt(str) { return parseInt(str, 10); } @@ -606,8 +586,9 @@ function padNumber(num, digits, trim) { } num = '' + num; while (num.length < digits) num = '0' + num; - if (trim) + if (trim) { num = num.substr(num.length - digits); + } return neg + num; } @@ -657,11 +638,12 @@ angular.mock.TzDate = function(offset, timestamp) { self.origDate = jsonStringToDate(timestamp); timestamp = self.origDate.getTime(); - if (isNaN(timestamp)) + if (isNaN(timestamp)) { throw { name: "Illegal Argument", message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" }; + } } else { self.origDate = new Date(timestamp); } @@ -789,13 +771,14 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) }; }); - $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser', - function($delegate, $$asyncCallback, $timeout, $browser) { + $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser', '$$rAF', + function($delegate, $$asyncCallback, $timeout, $browser, $$rAF) { var animate = { queue: [], cancel: $delegate.cancel, enabled: $delegate.enabled, triggerCallbackEvents: function() { + $$rAF.flush(); $$asyncCallback.flush(); }, triggerCallbackPromise: function() { @@ -1119,7 +1102,7 @@ angular.mock.dump = function(object) { ``` */ angular.mock.$HttpBackendProvider = function() { - this.$get = ['$rootScope', createHttpBackendMock]; + this.$get = ['$rootScope', '$timeout', createHttpBackendMock]; }; /** @@ -1136,7 +1119,7 @@ angular.mock.$HttpBackendProvider = function() { * @param {Object=} $browser Auto-flushing enabled if specified * @return {Object} Instance of $httpBackend mock */ -function createHttpBackendMock($rootScope, $delegate, $browser) { +function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { var definitions = [], expectations = [], responses = [], @@ -1149,7 +1132,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { return function() { return angular.isNumber(status) ? [status, data, headers, statusText] - : [200, status, data]; + : [200, status, data, headers]; }; } @@ -1166,7 +1149,9 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { } function wrapResponse(wrapped) { - if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); + if (!$browser && timeout) { + timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout); + } return handleResponse; @@ -1189,14 +1174,16 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { } if (expectation && expectation.match(method, url)) { - if (!expectation.matchData(data)) + if (!expectation.matchData(data)) { throw new Error('Expected ' + expectation + ' with different data\n' + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); + } - if (!expectation.matchHeaders(headers)) + if (!expectation.matchHeaders(headers)) { throw new Error('Expected ' + expectation + ' with different headers\n' + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); + } expectations.shift(); @@ -1232,8 +1219,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header @@ -1278,10 +1265,10 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1292,10 +1279,10 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1306,10 +1293,10 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1320,12 +1307,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1336,12 +1323,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1352,9 +1339,9 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1368,14 +1355,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new request expectation. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current expectation. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. * @@ -1407,10 +1394,10 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for GET requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. See #expect for more info. */ @@ -1421,10 +1408,10 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for HEAD requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1435,10 +1422,10 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for DELETE requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1449,13 +1436,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for POST requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1466,13 +1453,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PUT requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1483,13 +1470,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PATCH requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1500,9 +1487,9 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for JSONP requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * @param {string|RegExp|function(string)} url HTTP url or function that receives an url + * and returns true if the url matches the current definition. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. */ @@ -1778,7 +1765,7 @@ angular.mock.$RAFDecorator = ['$delegate', function($delegate) { queue[i](); } - queue = []; + queue = queue.slice(i); }; return rafFn; @@ -1807,6 +1794,77 @@ angular.mock.$RootElementProvider = function() { }; }; +/** + * @ngdoc service + * @name $controller + * @description + * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing + * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}. + * + * + * ## Example + * + * ```js + * + * // Directive definition ... + * + * myMod.directive('myDirective', { + * controller: 'MyDirectiveController', + * bindToController: { + * name: '@' + * } + * }); + * + * + * // Controller definition ... + * + * myMod.controller('MyDirectiveController', ['log', function($log) { + * $log.info(this.name); + * })]; + * + * + * // In a test ... + * + * describe('myDirectiveController', function() { + * it('should write the bound name to the log', inject(function($controller, $log) { + * var ctrl = $controller('MyDirective', { /* no locals */ }, { name: 'Clark Kent' }); + * expect(ctrl.name).toEqual('Clark Kent'); + * expect($log.info.logs).toEqual(['Clark Kent']); + * }); + * }); + * + * ``` + * + * @param {Function|string} constructor If called with a function then it's considered to be the + * controller constructor function. Otherwise it's considered to be a string which is used + * to retrieve the controller constructor using the following steps: + * + * * check if a controller with given name is registered via `$controllerProvider` + * * check if evaluating the string on the current scope returns a constructor + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (not recommended) + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. + * + * @param {Object} locals Injection locals for Controller. + * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used + * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @return {Object} Instance of given controller. + */ +angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { + return function(expression, locals, later, ident) { + if (later && typeof later === 'object') { + var create = $delegate(expression, locals, true, ident); + angular.extend(create.instance, later); + return create(); + } + return $delegate(expression, locals, later, ident); + }; +}]; + + /** * @ngdoc module * @name ngMock @@ -1835,6 +1893,7 @@ angular.module('ngMock', ['ng']).provider({ $provide.decorator('$$rAF', angular.mock.$RAFDecorator); $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); + $provide.decorator('$controller', angular.mock.$ControllerDecorator); }]); /** @@ -1911,8 +1970,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current definition. @@ -1939,8 +1998,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke @@ -1954,8 +2013,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke @@ -1969,8 +2028,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke @@ -1984,8 +2043,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -2000,8 +2059,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -2016,8 +2075,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PATCH requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -2032,15 +2091,15 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. */ angular.mock.e2e = {}; angular.mock.e2e.$httpBackendDecorator = - ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; + ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; /** @@ -2054,7 +2113,7 @@ angular.mock.e2e.$httpBackendDecorator = * * In addition to all the regular `Scope` methods, the following helper methods are available: */ -angular.mock.$RootScopeDecorator = function($delegate) { +angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { var $rootScopePrototype = Object.getPrototypeOf($delegate); @@ -2126,24 +2185,38 @@ angular.mock.$RootScopeDecorator = function($delegate) { return count; } -}; +}]; if (window.jasmine || window.mocha) { var currentSpec = null, + annotatedFunctions = [], isSpecRunning = function() { return !!currentSpec; }; + angular.mock.$$annotate = angular.injector.$$annotate; + angular.injector.$$annotate = function(fn) { + if (typeof fn === 'function' && !fn.$inject) { + annotatedFunctions.push(fn); + } + 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; @@ -2156,7 +2229,6 @@ if (window.jasmine || window.mocha) { if (injector) { injector.get('$rootElement').off(); - injector.get('$browser').pollFns.length = 0; } // clean up jquery's fragment cache diff --git a/public/vendor/angular-mocks/bower.json b/public/vendor/angular-mocks/bower.json new file mode 100644 index 00000000000..73aae58218b --- /dev/null +++ b/public/vendor/angular-mocks/bower.json @@ -0,0 +1,9 @@ +{ + "name": "angular-mocks", + "version": "1.4.0", + "main": "./angular-mocks.js", + "ignore": [], + "dependencies": { + "angular": "1.4.0" + } +} diff --git a/public/vendor/angular-mocks/ngAnimateMock.js b/public/vendor/angular-mocks/ngAnimateMock.js new file mode 100644 index 00000000000..6f99e62ef6a --- /dev/null +++ b/public/vendor/angular-mocks/ngAnimateMock.js @@ -0,0 +1,2 @@ +require('./angular-mocks'); +module.exports = 'ngAnimateMock'; diff --git a/public/vendor/angular-mocks/ngMock.js b/public/vendor/angular-mocks/ngMock.js new file mode 100644 index 00000000000..7944de7d5b1 --- /dev/null +++ b/public/vendor/angular-mocks/ngMock.js @@ -0,0 +1,2 @@ +require('./angular-mocks'); +module.exports = 'ngMock'; diff --git a/public/vendor/angular-mocks/ngMockE2E.js b/public/vendor/angular-mocks/ngMockE2E.js new file mode 100644 index 00000000000..fc2e539dbdc --- /dev/null +++ b/public/vendor/angular-mocks/ngMockE2E.js @@ -0,0 +1,2 @@ +require('./angular-mocks'); +module.exports = 'ngMockE2E'; diff --git a/public/vendor/angular-mocks/package.json b/public/vendor/angular-mocks/package.json new file mode 100644 index 00000000000..64bbd9c0b93 --- /dev/null +++ b/public/vendor/angular-mocks/package.json @@ -0,0 +1,27 @@ +{ + "name": "angular-mocks", + "version": "1.4.0", + "description": "AngularJS mocks for testing", + "main": "angular-mocks.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular/angular.js.git" + }, + "keywords": [ + "angular", + "framework", + "browser", + "mocks", + "testing", + "client-side" + ], + "author": "Angular Core Team ", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/angular.js/issues" + }, + "homepage": "http://angularjs.org" +} diff --git a/public/vendor/angular-native-dragdrop/.bower.json b/public/vendor/angular-native-dragdrop/.bower.json new file mode 100644 index 00000000000..d270f02ab32 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/.bower.json @@ -0,0 +1,37 @@ +{ + "name": "angular-native-dragdrop", + "version": "1.1.0", + "homepage": "http://angular-dragdrop.github.io/angular-dragdrop", + "authors": [ + "ganarajpr" + ], + "description": "Angular HTML5 Drag and Drop directive written in pure with no dependency on JQuery.", + "main": "draganddrop.js", + "keywords": [ + "angular", + "drag", + "drop", + "html5" + ], + "dependencies": { + "angular": "~1.3" + }, + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "_release": "1.1.0", + "_resolution": { + "type": "version", + "tag": "1.1.0", + "commit": "737981e86bd32a5432fa9edf09059ca3c0f22049" + }, + "_source": "git://github.com/angular-dragdrop/angular-dragdrop.git", + "_target": "~1.1.0", + "_originalSource": "angular-native-dragdrop", + "_direct": true +} \ No newline at end of file diff --git a/public/vendor/angular-native-dragdrop/Gulpfile.js b/public/vendor/angular-native-dragdrop/Gulpfile.js new file mode 100644 index 00000000000..0aa3bdfff72 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/Gulpfile.js @@ -0,0 +1,15 @@ +/* jshint -W097 */ +'use strict'; + +/* global require */ +var jshint = require('gulp-jshint'); +var stylish = require('jshint-stylish'); +var gulp = require('gulp'); + +gulp.task('lint', function() { + return gulp.src('./draganddrop.js') + .pipe(jshint()) + .pipe(jshint.reporter(stylish)); +}); + +gulp.task('default', ['lint']); diff --git a/public/vendor/angular-native-dragdrop/LICENSE b/public/vendor/angular-native-dragdrop/LICENSE new file mode 100644 index 00000000000..d239cb9e0d3 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/LICENSE @@ -0,0 +1,10 @@ + +The MIT License + +Copyright (c) 2015 Ganaraj P R, [Nebithi](http://www.nebithi.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/public/vendor/angular-native-dragdrop/README.md b/public/vendor/angular-native-dragdrop/README.md new file mode 100644 index 00000000000..e668ec4cef4 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/README.md @@ -0,0 +1,35 @@ +#Angular-DragDrop +[![Build status](http://img.shields.io/travis/angular-dragdrop/angular-dragdrop.svg?style=flat)](https://travis-ci.org/angular-dragdrop/angular-dragdrop) +[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/ganarajpr/angular-dragdrop?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +Angular-DragDrop is a angular HTML5 Drag and Drop directive written in pure with no dependency on JQuery. + +This is based on the work done by Jason Turim. While this [blog post](http://jasonturim.wordpress.com/2013/09/01/angularjs-drag-and-drop/) was the inspiration for creating a native Drag and Drop solution, the intention was to create something that was more generic. + +This implementation is mainly different from the one posted in the blog in the following areas : + +1. Angular-DragDrop does not create an isolate scope. This has huge benefits when it comes to working with other directives. **NOTE :** It also does not pollute the scope with any variables or functions. + +2. It does not depend on any kind of an ID attribute ( being either present or generated on the fly ). + +3. It allows one to create channels on which different drag and drop directive combinations can work on in the same page ( more on this later ) . + +Pull requests are welcome. + +[Documentation](http://angular-dragdrop.github.io/angular-dragdrop/) + +#Looking for Active Contributors. + +This repo needs active contributers and maintainers. If you are interested in being one of the people who would like to actively maintain this repo, please let me know. + + + +The MIT License + +Copyright (c) 2014 Ganaraj P R, [Nebithi](http://www.nebithi.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/public/vendor/angular-native-dragdrop/bower.json b/public/vendor/angular-native-dragdrop/bower.json new file mode 100644 index 00000000000..af96e3b4444 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/bower.json @@ -0,0 +1,27 @@ +{ + "name": "angular-native-dragdrop", + "version": "1.0.8", + "homepage": "http://angular-dragdrop.github.io/angular-dragdrop", + "authors": [ + "ganarajpr" + ], + "description": "Angular HTML5 Drag and Drop directive written in pure with no dependency on JQuery.", + "main": "draganddrop.js", + "keywords": [ + "angular", + "drag", + "drop", + "html5" + ], + "dependencies": { + "angular": "~1.3" + }, + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/public/vendor/angular-native-dragdrop/demo/css/styles.css b/public/vendor/angular-native-dragdrop/demo/css/styles.css new file mode 100644 index 00000000000..4dd18383697 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/demo/css/styles.css @@ -0,0 +1,29 @@ +body { + background-color: #f8f7f8; +} + +.heading { + border-bottom: 1px solid #b7b7b7; + padding-bottom: 10px; + margin-bottom: 10px; +} + +.topRow { + margin-bottom: 30px; +} + +.on-drag-enter { + background-color : #677ba6; +} + +.on-drag-enter-custom { + background-color : #d78cc7; +} + +.on-drag-hover { + background-color : #3eb352; +} + +.on-drag-hover-custom { + background-color : #d7a931; +} \ No newline at end of file diff --git a/public/vendor/angular-native-dragdrop/demo/index.html b/public/vendor/angular-native-dragdrop/demo/index.html new file mode 100644 index 00000000000..b945a19e835 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/demo/index.html @@ -0,0 +1,129 @@ + + + + + + Angular DragDrop (Demo) + + + + + + + + + + +
          +
          +

          + Drag and drop between the two lists. +

          +
          + +

          Beasts

          + +

          Left column of beasts is not draggable and accepts both beasts and priests

          + +
          + +
          +
          +
            +
          • + {{man}} +
          • +
          +
          +
          +
            +
          • + {{woman}} +
          • +
          +
          +
          + +
          + +

          Priests

          + +
          + +
          +
          +
            +
          • + {{man}} +
          • +
          +
          +
          +
            +
          • + {{woman}} +
          • +
          +
          +
          + +
          + +

          Terrorists

          + +

          Each terrorist list item accepts a new terrorist. Shows inserting into a particular + position in an array.

          + +
          + + +
          +
          +
            +
          • + {{man}} +
          • +
          +
          +
          +
            +
          • + {{woman}} +
          • +
          +
          +
          +
          + + + diff --git a/public/vendor/angular-native-dragdrop/demo/js/app.js b/public/vendor/angular-native-dragdrop/demo/js/app.js new file mode 100644 index 00000000000..7fca20ac0ce --- /dev/null +++ b/public/vendor/angular-native-dragdrop/demo/js/app.js @@ -0,0 +1,46 @@ +angular.module('app', [ + 'hljs', + 'ang-drag-drop' +]).controller('MainCtrl', function($scope) { + $scope.men = [ + 'John', + 'Jack', + 'Mark', + 'Ernie', + 'Mike (Locked)' + ]; + + + $scope.women = [ + 'Jane', + 'Jill', + 'Betty', + 'Mary' + ]; + + $scope.addText = ''; + + $scope.dropValidateHandler = function($drop, $event, $data) { + if ($data === 'Mike (Locked)') { + return false; + } + if ($drop.element[0] === $event.srcElement.parentNode) { + // Don't allow moving to same container + return false; + } + return true; + }; + + $scope.dropSuccessHandler = function($event, index, array) { + array.splice(index, 1); + }; + + $scope.onDrop = function($event, $data, array, index) { + if (index !== undefined) { + array.splice(index, 0, $data); + } else { + array.push($data); + } + }; + +}); diff --git a/public/vendor/angular-native-dragdrop/docs/css/styles.css b/public/vendor/angular-native-dragdrop/docs/css/styles.css new file mode 100644 index 00000000000..d4480e8b536 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/docs/css/styles.css @@ -0,0 +1,32 @@ +.content { + margin : 50px auto; + font-size: 1.0em; + line-height: 1.5em; +} + +.content a{ + color: #677BA6; + cursor: pointer; +} + +body { + background-color: #f8f7f8; +} + +.jumbotron h1, +.jumbotron p{ + font-family: 'Open Sans'; +} + +.heading{ + border-bottom: 1px solid #b7b7b7; + padding-bottom: 10px; + margin-bottom: 10px; +} + +.ribbon{ + position: fixed; + top : 0; + right : 0; + z-index: 2000; +} \ No newline at end of file diff --git a/public/vendor/angular-native-dragdrop/docs/index.html b/public/vendor/angular-native-dragdrop/docs/index.html new file mode 100644 index 00000000000..25603db4e34 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/docs/index.html @@ -0,0 +1,323 @@ + + + + Angular DragDrop + + + + + + + + + + +Fork me on GitHub +
          +
          +

          Angular Drag and Drop

          + +

          Angular-DragDrop is a AngularJS HTML5 Drag and Drop directive written in pure with no dependency on JQuery.

          + + +
          + + +
          +
          +

          Directives

          +
          + +

          ui-draggable

          + +

          + directive in module ngDragDrop +

          + +

          The ui-draggable attribute tells Angular that the element is draggable. ui-draggable + takes an expression as the attribute value. The expression should evaluate to either true or false. + You can toggle the draggability of an element using this expression. +

          + + +

          Additional Attributes

          + +

          drag

          + +

          The drag property is used to assign the data that needs to be passed along with the dragging + element.

          +
          +

          drag-handle-class

          + +

          The class used to mark child elements of draggable object to be used as drag handle. Default class name is + drag-handle.

          +
          + NOTE: If attribute is not present drag handle feature is not active. +
          +
          +

          on-drop-success

          + +

          The on-drop-success attribute takes a function. We can consider this to be an on-drop-success + handler function. + This can be useful if you need to do some post processing after the dragged element is dropped successfully on + the drop site. + +

          + NOTE: This callback function is only called when the drop succeeds. +
          + You can request the drag-end event ( very similiar to requesting the click event in + ng-click ) + by passing $event in the event handler. +

          + +
          +

          on-drop-failure

          + +

          The on-drop-failure attribute takes a function. We can consider this to be an on-drop-failure + handler function. + This can be useful if you need to do some post processing after the dragged element is dropped unsuccessfully on + any drop site. + +

          + NOTE: This callback function is only called when the drop fails. +
          + You can request the drag-end event ( very similiar to requesting the click event in + ng-click ) + by passing $event in the event handler. +

          + + +
          +

          Usage

          + +

          + +

          + ... + +
          +

          + +

          Details

          + +

          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          ParamTypeDetails
          ui-draggableExpression that should be + evaluated. The given expression should resolve to true or false. +
          dragTakes any JSON convertable $scope variable.
          drag-handle-classClass name used to mark child elements of draggable object to be used as drag handle.
          If attribute + is not present drag handle feature is not used.
          If attribute is present but have no value + drag-handle used as default.
          on-drop-successTakes any $scope function. Can also pass $event.
          on-drop-failureTakes any $scope function. Can also pass $event.
          drag-channelTakes a string that can be used as the channel name for the dragging operation. + Default channel is "defaultchannel" +
          +

          +
          + +

          Events

          + +

          On start of dragging an Angular Event ANGULAR_DRAG_START is dispatched from the + $rootScope. The event also carries + carries the information about the channel in which the dragging has started. +

          + +

          On end of dragging an Angular Event ANGULAR_DRAG_END is dispatched from the $rootScope. + The event also carries + carries the information about the channel in which the dragging has started. +

          + +

          When hovering a draggable element on top of a drop area an Angular Event ANGULAR_HOVER + is dispatched from the $rootScope. + The event also carries the information about the channel in which the dragging has started. +

          + +
          + +

          ui-on-drop

          + +

          + directive in module ngDragDrop +

          + +

          The ui-on-drop attribute tells Angular that the element is a drop site. ui-on-drop + takes a function as the attribute value. The function will be called when a valid dragged element is dropped in + that location. + A valid dragged element is one which has the same channel as the drop location. + +

          + NOTE : This callback function is only called when the drop succeeds. +
          + The ui-on-drop callback can request additional parameters. The data that is dragged is available to the + callback as $data and its channel as $channel. Apart from this the drop event is exposed as $event. +

          +

          Additional Attributes

          + +

          drop-channel

          + +

          The channel that the drop site accepts. The dragged element should have the same channel as this drop site for it + to be droppable at this location. It is possible to provide comma separated list of channels. + +

          + NOTE: Also special value of drag-channel attribute is available to accept + dragged element with any channel value — * +
          +

          + +
          + +

          drop-validate

          + +

          Extra validation that makes sure that the drop site accepts the dragged element beyond having the same channel. If + not defined, no extra validation is made. + +

          + NOTE: This callback function is called only if the channel condition is met, when the element + starts being dragged +
          +

          + +
          + +

          drag-enter-class

          + +

          The class that will be added to the the droppable element when a dragged element ( which is droppable ) + enters the drop location. The default value for this is on-drag-enter

          + +

          drag-hover-class

          + +

          The class that will be added to the drop area element when hovering with an element. + The default value for this is on-drag-hover

          + +
          +

          Usage

          + +

          + +

          + ... +
          +

          + +

          Details

          + +

          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          ParamTypeDetails
          ui-on-dropTakes any $scope function. Can also pass $event, $data and $channel. +
          drop-channelThe channel on which the drop has to listen for drag events.
          + Single value, comma separated list or special value * are possible
          drop-validateTakes any $scope function. Can also pass $data and $channel +
          drag-enter-classA class name that will be put on the droppable element when the dragged objects enters its boundaries. +
          Default class name is on-drag-enter.
          drag-hover-classA class name that will be put on the drop area when an element is dragged onto it.
          Default class + name is on-drag-hover.
          +

          +
          +

          Demo

          + + + +
          +
          + + + + + diff --git a/public/vendor/angular-native-dragdrop/draganddrop.js b/public/vendor/angular-native-dragdrop/draganddrop.js new file mode 100644 index 00000000000..f45b47df4d9 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/draganddrop.js @@ -0,0 +1,370 @@ +(function(angular) { + 'use strict'; + + function isDnDsSupported() { + return 'ondrag' in document.createElement('a'); + } + + if (!isDnDsSupported()) { + angular.module('ang-drag-drop', []); + return; + } + + if (window.jQuery && (-1 === window.jQuery.event.props.indexOf('dataTransfer'))) { + window.jQuery.event.props.push('dataTransfer'); + } + + var module = angular.module('ang-drag-drop', []); + + module.directive('uiDraggable', ['$parse', '$rootScope', '$dragImage', function($parse, $rootScope, $dragImage) { + return function(scope, element, attrs) { + var isDragHandleUsed = false, + dragHandleClass, + draggingClass = attrs.draggingClass || 'on-dragging', + dragTarget; + + element.attr('draggable', false); + + scope.$watch(attrs.uiDraggable, function(newValue) { + if (newValue) { + element.attr('draggable', newValue); + element.bind('dragend', dragendHandler); + element.bind('dragstart', dragstartHandler); + } + else { + element.removeAttr('draggable'); + element.unbind('dragend', dragendHandler); + element.unbind('dragstart', dragstartHandler); + } + + }); + + if (angular.isString(attrs.dragHandleClass)) { + isDragHandleUsed = true; + dragHandleClass = attrs.dragHandleClass.trim() || 'drag-handle'; + + element.bind('mousedown', function(e) { + dragTarget = e.target; + }); + } + + function dragendHandler(e) { + setTimeout(function() { + element.unbind('$destroy', dragendHandler); + }, 0); + var sendChannel = attrs.dragChannel || 'defaultchannel'; + $rootScope.$broadcast('ANGULAR_DRAG_END', e, sendChannel); + if (e.dataTransfer && e.dataTransfer.dropEffect !== 'none') { + if (attrs.onDropSuccess) { + var onDropSuccessFn = $parse(attrs.onDropSuccess); + scope.$evalAsync(function() { + onDropSuccessFn(scope, {$event: e}); + }); + } else { + if (attrs.onDropFailure) { + var onDropFailureFn = $parse(attrs.onDropFailure); + scope.$evalAsync(function() { + onDropFailureFn(scope, {$event: e}); + }); + } + } + } + element.removeClass(draggingClass); + } + + function dragstartHandler(e) { + var isDragAllowed = !isDragHandleUsed || dragTarget.classList.contains(dragHandleClass); + + if (isDragAllowed) { + var sendChannel = attrs.dragChannel || 'defaultchannel'; + var dragData = ''; + if (attrs.drag) { + dragData = scope.$eval(attrs.drag); + } + + var dragImage = attrs.dragImage || null; + + element.addClass(draggingClass); + element.bind('$destroy', dragendHandler); + + if (dragImage) { + var dragImageFn = $parse(attrs.dragImage); + scope.$apply(function() { + var dragImageParameters = dragImageFn(scope, {$event: e}); + if (dragImageParameters) { + if (angular.isString(dragImageParameters)) { + dragImageParameters = $dragImage.generate(dragImageParameters); + } + if (dragImageParameters.image) { + var xOffset = dragImageParameters.xOffset || 0, + yOffset = dragImageParameters.yOffset || 0; + e.dataTransfer.setDragImage(dragImageParameters.image, xOffset, yOffset); + } + } + }); + } + + var transferDataObject = {data: dragData, channel: sendChannel} + var transferDataText = angular.toJson(transferDataObject); + + e.dataTransfer.setData('text', transferDataText); + e.dataTransfer.effectAllowed = 'copyMove'; + + $rootScope.$broadcast('ANGULAR_DRAG_START', e, sendChannel, transferDataObject); + } + else { + e.preventDefault(); + } + } + }; + } + ]); + + module.directive('uiOnDrop', ['$parse', '$rootScope', function($parse, $rootScope) { + return function(scope, element, attr) { + var dragging = 0; //Ref. http://stackoverflow.com/a/10906204 + var dropChannel = attr.dropChannel || 'defaultchannel'; + var dragChannel = ''; + var dragEnterClass = attr.dragEnterClass || 'on-drag-enter'; + var dragHoverClass = attr.dragHoverClass || 'on-drag-hover'; + var customDragEnterEvent = $parse(attr.onDragEnter); + var customDragLeaveEvent = $parse(attr.onDragLeave); + + function onDragOver(e) { + if (e.preventDefault) { + e.preventDefault(); // Necessary. Allows us to drop. + } + + if (e.stopPropagation) { + e.stopPropagation(); + } + + var uiOnDragOverFn = $parse(attr.uiOnDragOver); + scope.$evalAsync(function() { + uiOnDragOverFn(scope, {$event: e, $channel: dropChannel}); + }); + + return false; + } + + function onDragLeave(e) { + if (e.preventDefault) { + e.preventDefault(); + } + + if (e.stopPropagation) { + e.stopPropagation(); + } + dragging--; + + if (dragging === 0) { + scope.$evalAsync(function() { + customDragLeaveEvent(scope, {$event: e, $channel: dropChannel}); + }); + element.addClass(dragEnterClass); + element.removeClass(dragHoverClass); + } + + var uiOnDragLeaveFn = $parse(attr.uiOnDragLeave); + scope.$evalAsync(function() { + uiOnDragLeaveFn(scope, {$event: e, $channel: dropChannel}); + }); + } + + function onDragEnter(e) { + if (e.preventDefault) { + e.preventDefault(); + } + + if (e.stopPropagation) { + e.stopPropagation(); + } + + if (dragging === 0) { + scope.$evalAsync(function() { + customDragEnterEvent(scope, {$event: e, $channel: dropChannel}); + }); + element.removeClass(dragEnterClass); + element.addClass(dragHoverClass); + } + dragging++; + + var uiOnDragEnterFn = $parse(attr.uiOnDragEnter); + scope.$evalAsync(function() { + uiOnDragEnterFn(scope, {$event: e, $channel: dropChannel}); + }); + + $rootScope.$broadcast('ANGULAR_HOVER', dragChannel); + } + + function onDrop(e) { + if (e.preventDefault) { + e.preventDefault(); // Necessary. Allows us to drop. + } + if (e.stopPropagation) { + e.stopPropagation(); // Necessary. Allows us to drop. + } + + var sendData = e.dataTransfer.getData('text'); + sendData = angular.fromJson(sendData); + + // Chrome doesn't set dropEffect, so we have to work it out ourselves + if (e.dataTransfer.dropEffect === 'none') { + if (e.dataTransfer.effectAllowed === 'copy' || + e.dataTransfer.effectAllowed === 'move') { + e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed; + } else if (e.dataTransfer.effectAllowed === 'copyMove') { + e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move'; + } + } + + var uiOnDropFn = $parse(attr.uiOnDrop); + scope.$evalAsync(function() { + uiOnDropFn(scope, {$data: sendData.data, $event: e, $channel: sendData.channel}); + }); + element.removeClass(dragEnterClass); + dragging = 0; + } + + function isDragChannelAccepted(dragChannel, dropChannel) { + if (dropChannel === '*') { + return true; + } + + var channelMatchPattern = new RegExp('(\\s|[,])+(' + dragChannel + ')(\\s|[,])+', 'i'); + + return channelMatchPattern.test(',' + dropChannel + ','); + } + + function preventNativeDnD(e) { + if (e.preventDefault) { + e.preventDefault(); + } + if (e.stopPropagation) { + e.stopPropagation(); + } + e.dataTransfer.dropEffect = 'none'; + return false; + } + + var deregisterDragStart = $rootScope.$on('ANGULAR_DRAG_START', function(_, e, channel, transferDataObject) { + dragChannel = channel; + + var valid = true; + + if (!isDragChannelAccepted(channel, dropChannel)) { + valid = false; + } + + if (valid && attr.dropValidate) { + var validateFn = $parse(attr.dropValidate); + valid = validateFn(scope, {$drop: {scope: scope, element:element}, $event:e, $data: transferDataObject.data, $channel: transferDataObject.channel}); + } + + if (valid) { + element.bind('dragover', onDragOver); + element.bind('dragenter', onDragEnter); + element.bind('dragleave', onDragLeave); + element.bind('drop', onDrop); + + element.addClass(dragEnterClass); + } else { + element.bind('dragover', preventNativeDnD); + element.bind('dragenter', preventNativeDnD); + element.bind('dragleave', preventNativeDnD); + element.bind('drop', preventNativeDnD); + + element.removeClass(dragEnterClass); + } + + }); + + + var deregisterDragEnd = $rootScope.$on('ANGULAR_DRAG_END', function(_, e, channel) { + element.unbind('dragover', onDragOver); + element.unbind('dragenter', onDragEnter); + element.unbind('dragleave', onDragLeave); + + element.unbind('drop', onDrop); + element.removeClass(dragHoverClass); + element.removeClass(dragEnterClass); + + element.unbind('dragover', preventNativeDnD); + element.unbind('dragenter', preventNativeDnD); + element.unbind('dragleave', preventNativeDnD); + element.unbind('drop', preventNativeDnD); + }); + + scope.$on('$destroy', function() { + deregisterDragStart(); + deregisterDragEnd(); + }); + + + attr.$observe('dropChannel', function(value) { + if (value) { + dropChannel = value; + } + }); + + + }; + } + ]); + + module.constant('$dragImageConfig', { + height: 20, + width: 200, + padding: 10, + font: 'bold 11px Arial', + fontColor: '#eee8d5', + backgroundColor: '#93a1a1', + xOffset: 0, + yOffset: 0 + }); + + module.service('$dragImage', ['$dragImageConfig', function(defaultConfig) { + var ELLIPSIS = '…'; + + function fitString(canvas, text, config) { + var width = canvas.measureText(text).width; + if (width < config.width) { + return text; + } + while (width + config.padding > config.width) { + text = text.substring(0, text.length - 1); + width = canvas.measureText(text + ELLIPSIS).width; + } + return text + ELLIPSIS; + } + + this.generate = function(text, options) { + var config = angular.extend({}, defaultConfig, options || {}); + var el = document.createElement('canvas'); + + el.height = config.height; + el.width = config.width; + + var canvas = el.getContext('2d'); + + canvas.fillStyle = config.backgroundColor; + canvas.fillRect(0, 0, config.width, config.height); + canvas.font = config.font; + canvas.fillStyle = config.fontColor; + + var title = fitString(canvas, text, config); + canvas.fillText(title, 4, config.padding + 4); + + var image = new Image(); + image.src = el.toDataURL(); + + return { + image: image, + xOffset: config.xOffset, + yOffset: config.yOffset + }; + }; + } + ]); + +}(angular)); diff --git a/public/vendor/angular-native-dragdrop/package.json b/public/vendor/angular-native-dragdrop/package.json new file mode 100644 index 00000000000..4e9871d3ee7 --- /dev/null +++ b/public/vendor/angular-native-dragdrop/package.json @@ -0,0 +1,24 @@ +{ + "name": "angular-native-dragdrop", + "version": "1.0.8", + "description": "Angular HTML5 Drag and Drop directive written in pure with no dependency on JQuery.", + "main": "draganddrop.js", + "scripts": { + "test": "gulp" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular-dragdrop/angular-dragdrop.git" + }, + "author": "ganarajpr", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular-dragdrop/angular-dragdrop/issues" + }, + "homepage": "http://angular-dragdrop.github.io/angular-dragdrop", + "devDependencies": { + "gulp": "^3.8.11", + "gulp-jshint": "^1.9.2", + "jshint-stylish": "^1.0.1" + } +} diff --git a/public/vendor/angular/angular-strap.js b/public/vendor/angular-other/angular-strap.js similarity index 100% rename from public/vendor/angular/angular-strap.js rename to public/vendor/angular-other/angular-strap.js diff --git a/public/vendor/angular/datepicker.js b/public/vendor/angular-other/datepicker.js similarity index 100% rename from public/vendor/angular/datepicker.js rename to public/vendor/angular-other/datepicker.js diff --git a/public/vendor/angular/timepicker.js b/public/vendor/angular-other/timepicker.js similarity index 100% rename from public/vendor/angular/timepicker.js rename to public/vendor/angular-other/timepicker.js diff --git a/public/vendor/angular-route/.bower.json b/public/vendor/angular-route/.bower.json new file mode 100644 index 00000000000..03fa8e04c88 --- /dev/null +++ b/public/vendor/angular-route/.bower.json @@ -0,0 +1,20 @@ +{ + "name": "angular-route", + "version": "1.4.0", + "main": "./angular-route.js", + "ignore": [], + "dependencies": { + "angular": "1.4.0" + }, + "homepage": "https://github.com/angular/bower-angular-route", + "_release": "1.4.0", + "_resolution": { + "type": "version", + "tag": "v1.4.0", + "commit": "af773f99661df8a9ca9275d123a1daf6cc0bf778" + }, + "_source": "git://github.com/angular/bower-angular-route.git", + "_target": "~1.4.0", + "_originalSource": "angular-route", + "_direct": true +} \ No newline at end of file diff --git a/public/vendor/angular-route/README.md b/public/vendor/angular-route/README.md new file mode 100644 index 00000000000..2cd4f9091d4 --- /dev/null +++ b/public/vendor/angular-route/README.md @@ -0,0 +1,68 @@ +# packaged angular-route + +This repo is for distribution on `npm` and `bower`. The source for this module is in the +[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngRoute). +Please file issues and pull requests against that repo. + +## Install + +You can install this package either with `npm` or with `bower`. + +### npm + +```shell +npm install angular-route +``` + +Then add `ngRoute` as a dependency for your app: + +```javascript +angular.module('myApp', [require('angular-route')]); +``` + +### bower + +```shell +bower install angular-route +``` + +Add a ` +``` + +Then add `ngRoute` as a dependency for your app: + +```javascript +angular.module('myApp', ['ngRoute']); +``` + +## Documentation + +Documentation is available on the +[AngularJS docs site](http://docs.angularjs.org/api/ngRoute). + +## License + +The MIT License + +Copyright (c) 2010-2015 Google, Inc. http://angularjs.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/vendor/angular/angular-route.js b/public/vendor/angular-route/angular-route.js similarity index 97% rename from public/vendor/angular/angular-route.js rename to public/vendor/angular-route/angular-route.js index 68203187841..67e04fcc1da 100644 --- a/public/vendor/angular/angular-route.js +++ b/public/vendor/angular-route/angular-route.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.4 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.0 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -78,8 +78,8 @@ function $RouteProvider() { * - `controller` – `{(string|function()=}` – Controller fn that should be associated with * newly created scope or the name of a {@link angular.Module#controller registered * controller} if passed as a string. - * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be - * published to scope under the `controllerAs` name. + * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. + * If present, the controller will be published to scope under the `controllerAs` name. * - `template` – `{string=|function()=}` – html template as a string or a function that * returns an html template as a string which should be used by {@link * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. @@ -180,7 +180,7 @@ function $RouteProvider() { * @description * * A boolean property indicating if routes defined - * using this provider should be matched using a case sensitive + * using this provider should be matched using a case insensitive * algorithm. Defaults to `false`. */ this.caseInsensitiveMatch = false; @@ -440,9 +440,11 @@ function $RouteProvider() { * @name $route#$routeUpdate * @eventType broadcast on root scope * @description - * * The `reloadOnSearch` property has been set to false, and we are reusing the same * instance of the Controller. + * + * @param {Object} angularEvent Synthetic event object + * @param {Route} current Current/previous route information. */ var forceReload = false, @@ -482,21 +484,15 @@ function $RouteProvider() { * definitions will be interpolated into the location's path, while * remaining properties will be treated as query params. * - * @param {Object} newParams mapping of URL parameter names to values + * @param {!Object} newParams mapping of URL parameter names to values */ updateParams: function(newParams) { if (this.current && this.current.$$route) { - var searchParams = {}, self=this; - - angular.forEach(Object.keys(newParams), function(key) { - if (!self.current.pathParams[key]) searchParams[key] = newParams[key]; - }); - newParams = angular.extend({}, this.current.params, newParams); $location.path(interpolate(this.current.$$route.originalPath, newParams)); - $location.search(angular.extend({}, $location.search(), searchParams)); - } - else { + // interpolate modifies newParams, only query params are left + $location.search(newParams); + } else { throw $routeMinErr('norout', 'Tried updating route when with no current route'); } } @@ -612,8 +608,8 @@ function $RouteProvider() { return $q.all(locals); } }). - // after route change then(function(locals) { + // after route change if (nextRoute == $route.current) { if (nextRoute) { nextRoute.locals = locals; @@ -788,7 +784,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); .view-animate-container { position:relative; height:100px!important; - position:relative; background:white; border:1px solid black; height:40px; diff --git a/public/vendor/angular-route/angular-route.min.js b/public/vendor/angular-route/angular-route.min.js new file mode 100644 index 00000000000..f43d0bf4118 --- /dev/null +++ b/public/vendor/angular-route/angular-route.min.js @@ -0,0 +1,15 @@ +/* + AngularJS v1.4.0 + (c) 2010-2015 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(q,d,C){'use strict';function v(r,k,h){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,f,b,c,y){function z(){l&&(h.cancel(l),l=null);m&&(m.$destroy(),m=null);n&&(l=h.leave(n),l.then(function(){l=null}),n=null)}function x(){var b=r.current&&r.current.locals;if(d.isDefined(b&&b.$template)){var b=a.$new(),c=r.current;n=y(b,function(b){h.enter(b,null,n||f).then(function(){!d.isDefined(t)||t&&!a.$eval(t)||k()});z()});m=c.scope=b;m.$emit("$viewContentLoaded"); +m.$eval(w)}else z()}var m,n,l,t=b.autoscroll,w=b.onload||"";a.$on("$routeChangeSuccess",x);x()}}}function A(d,k,h){return{restrict:"ECA",priority:-400,link:function(a,f){var b=h.current,c=b.locals;f.html(c.$template);var y=d(f.contents());b.controller&&(c.$scope=a,c=k(b.controller,c),b.controllerAs&&(a[b.controllerAs]=c),f.data("$ngControllerController",c),f.children().data("$ngControllerController",c));y(a)}}}q=d.module("ngRoute",["ng"]).provider("$route",function(){function r(a,f){return d.extend(Object.create(a), +f)}function k(a,d){var b=d.caseInsensitiveMatch,c={originalPath:a,regexp:a},h=c.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,d,b,c){a="?"===c?c:null;c="*"===c?c:null;h.push({name:b,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");c.regexp=new RegExp("^"+a+"$",b?"i":"");return c}var h={};this.when=function(a,f){var b=d.copy(f);d.isUndefined(b.reloadOnSearch)&&(b.reloadOnSearch=!0); +d.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);h[a]=d.extend(b,a&&k(a,b));if(a){var c="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";h[c]=d.extend({redirectTo:a},k(c,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,f,b,c,k,q,x){function m(b){var e=s.current; +(v=(p=l())&&e&&p.$$route===e.$$route&&d.equals(p.pathParams,e.pathParams)&&!p.reloadOnSearch&&!w)||!e&&!p||a.$broadcast("$routeChangeStart",p,e).defaultPrevented&&b&&b.preventDefault()}function n(){var u=s.current,e=p;if(v)u.params=e.params,d.copy(u.params,b),a.$broadcast("$routeUpdate",u);else if(e||u)w=!1,(s.current=e)&&e.redirectTo&&(d.isString(e.redirectTo)?f.path(t(e.redirectTo,e.params)).search(e.params).replace():f.url(e.redirectTo(e.pathParams,f.path(),f.search())).replace()),c.when(e).then(function(){if(e){var a= +d.extend({},e.resolve),b,g;d.forEach(a,function(b,e){a[e]=d.isString(b)?k.get(b):k.invoke(b,null,null,e)});d.isDefined(b=e.template)?d.isFunction(b)&&(b=b(e.params)):d.isDefined(g=e.templateUrl)&&(d.isFunction(g)&&(g=g(e.params)),g=x.getTrustedResourceUrl(g),d.isDefined(g)&&(e.loadedTemplateUrl=g,b=q(g)));d.isDefined(b)&&(a.$template=b);return c.all(a)}}).then(function(c){e==s.current&&(e&&(e.locals=c,d.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,u))},function(b){e==s.current&&a.$broadcast("$routeChangeError", +e,u,b)})}function l(){var a,b;d.forEach(h,function(c,h){var g;if(g=!b){var k=f.path();g=c.keys;var m={};if(c.regexp)if(k=c.regexp.exec(k)){for(var l=1,n=k.length;l", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/angular.js/issues" + }, + "homepage": "http://angularjs.org" +} diff --git a/public/vendor/angular-sanitize/.bower.json b/public/vendor/angular-sanitize/.bower.json new file mode 100644 index 00000000000..efc6c2d3224 --- /dev/null +++ b/public/vendor/angular-sanitize/.bower.json @@ -0,0 +1,20 @@ +{ + "name": "angular-sanitize", + "version": "1.4.0", + "main": "./angular-sanitize.js", + "ignore": [], + "dependencies": { + "angular": "1.4.0" + }, + "homepage": "https://github.com/angular/bower-angular-sanitize", + "_release": "1.4.0", + "_resolution": { + "type": "version", + "tag": "v1.4.0", + "commit": "a64d96eff0b9f15db70322e77bc20c2e64bd8e07" + }, + "_source": "git://github.com/angular/bower-angular-sanitize.git", + "_target": "~1.4.0", + "_originalSource": "angular-sanitize", + "_direct": true +} \ No newline at end of file diff --git a/public/vendor/angular-sanitize/README.md b/public/vendor/angular-sanitize/README.md new file mode 100644 index 00000000000..b84aaf6dbf1 --- /dev/null +++ b/public/vendor/angular-sanitize/README.md @@ -0,0 +1,68 @@ +# packaged angular-sanitize + +This repo is for distribution on `npm` and `bower`. The source for this module is in the +[main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngSanitize). +Please file issues and pull requests against that repo. + +## Install + +You can install this package either with `npm` or with `bower`. + +### npm + +```shell +npm install angular-sanitize +``` + +Then add `ngSanitize` as a dependency for your app: + +```javascript +angular.module('myApp', [require('angular-sanitize')]); +``` + +### bower + +```shell +bower install angular-sanitize +``` + +Add a ` +``` + +Then add `ngSanitize` as a dependency for your app: + +```javascript +angular.module('myApp', ['ngSanitize']); +``` + +## Documentation + +Documentation is available on the +[AngularJS docs site](http://docs.angularjs.org/api/ngSanitize). + +## License + +The MIT License + +Copyright (c) 2010-2015 Google, Inc. http://angularjs.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/vendor/angular/angular-sanitize.js b/public/vendor/angular-sanitize/angular-sanitize.js similarity index 83% rename from public/vendor/angular/angular-sanitize.js rename to public/vendor/angular-sanitize/angular-sanitize.js index f463a8c9586..fd3aad02f0f 100644 --- a/public/vendor/angular/angular-sanitize.js +++ b/public/vendor/angular-sanitize/angular-sanitize.js @@ -1,10 +1,21 @@ /** - * @license AngularJS v1.3.4 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.0 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + var $sanitizeMinErr = angular.$$minErr('$sanitize'); /** @@ -200,10 +211,11 @@ var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a // SVG Elements // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements -var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + - "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + - "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + - "stop,svg,switch,text,title,tspan,use"); +// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. +// They can potentially allow for arbitrary javascript to be executed. See #11290 +var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," + + "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," + + "radialGradient,rect,stop,svg,switch,text,title,tspan,use"); // Special Elements (can contain anything) var specialElements = makeMap("script,style"); @@ -227,30 +239,31 @@ var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspac // SVG attributes (without "id" and "name" attributes) // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + - 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + - 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + - 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + - 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + - 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + - 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + - 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + - 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + - 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + - 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + - 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + - 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + - 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + - 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + - 'zoomAndPan'); + 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + + 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + + 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + + 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + + 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + + 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + + 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + + 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + + 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + + 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + + 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + + 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + + 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + + 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); var validAttrs = angular.extend({}, uriAttrs, svgAttrs, htmlAttrs); -function makeMap(str) { +function makeMap(str, lowercaseKeys) { var obj = {}, items = str.split(','), i; - for (i = 0; i < items.length; i++) obj[items[i]] = true; + for (i = 0; i < items.length; i++) { + obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true; + } return obj; } @@ -276,14 +289,14 @@ function htmlParser(html, handler) { } } var index, chars, match, stack = [], last = html, text; - stack.last = function() { return stack[ stack.length - 1 ]; }; + stack.last = function() { return stack[stack.length - 1]; }; while (html) { text = ''; chars = true; // Make sure we're not in a script or style element - if (!stack.last() || !specialElements[ stack.last() ]) { + if (!stack.last() || !specialElements[stack.last()]) { // Comment if (html.indexOf(" + *
          + * *
          * ``` * - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as disabled. (Their presence means true and their absence means false.) + * This is because the HTML specification does not require browsers to preserve the values of + * boolean attributes such as `disabled` (Their presence means true and their absence means false.) * If we put an Angular interpolation expression into such an attribute then the * binding information would be lost when the browser removes the attribute. - * The `ngDisabled` directive solves this problem for the `disabled` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. * * @example - Click me to toggle:
          +
          @@ -17763,7 +19418,7 @@ var htmlAnchorDirective = valueFn({ * * @element INPUT * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, - * then special attribute "disabled" will be set on the element + * then the `disabled` attribute will be set on the element */ @@ -17784,8 +19439,8 @@ var htmlAnchorDirective = valueFn({ * @example - Check me to check both:
          - +
          +
          it('should check both checkBoxes', function() { @@ -17819,8 +19474,8 @@ var htmlAnchorDirective = valueFn({ * @example - Check me to make text readonly:
          - +
          +
          it('should toggle readonly attr', function() { @@ -17855,8 +19510,8 @@ var htmlAnchorDirective = valueFn({ * @example - Check me to select:
          -
          + @@ -17892,7 +19547,7 @@ var htmlAnchorDirective = valueFn({ * @example - Check me check multiple:
          +
          Show/Hide me
          @@ -17913,22 +19568,34 @@ var htmlAnchorDirective = valueFn({ var ngAttributeAliasDirectives = {}; - // boolean attrs are evaluated forEach(BOOLEAN_ATTR, function(propName, attrName) { // binding to multiple is not supported if (propName == "multiple") return; + function defaultLinkFn(scope, element, attr) { + scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { + attr.$set(attrName, !!value); + }); + } + var normalized = directiveNormalize('ng-' + attrName); + var linkFn = defaultLinkFn; + + if (propName === 'checked') { + linkFn = function(scope, element, attr) { + // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input + if (attr.ngModel !== attr[normalized]) { + defaultLinkFn(scope, element, attr); + } + }; + } + ngAttributeAliasDirectives[normalized] = function() { return { restrict: 'A', priority: 100, - link: function(scope, element, attr) { - scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { - attr.$set(attrName, !!value); - }); - } + link: linkFn }; }; }); @@ -18158,6 +19825,9 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { forEach(form.$error, function(value, name) { form.$setValidity(name, null, control); }); + forEach(form.$$success, function(value, name) { + form.$setValidity(name, null, control); + }); arrayRemove(controls, control); }; @@ -18175,23 +19845,23 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { addSetValidityMethod({ ctrl: this, $element: element, - set: function(object, property, control) { + set: function(object, property, controller) { var list = object[property]; if (!list) { - object[property] = [control]; + object[property] = [controller]; } else { - var index = list.indexOf(control); + var index = list.indexOf(controller); if (index === -1) { - list.push(control); + list.push(controller); } } }, - unset: function(object, property, control) { + unset: function(object, property, controller) { var list = object[property]; if (!list) { return; } - arrayRemove(list, control); + arrayRemove(list, controller); if (list.length === 0) { delete object[property]; } @@ -18308,7 +19978,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * * # Alias: {@link ng.directive:ngForm `ngForm`} * - * In Angular forms can be nested. This means that the outer form is valid when all of the child + * In Angular, forms can be nested. This means that the outer form is valid when all of the child * forms are valid as well. However, browsers do not allow nesting of `
          ` elements, so * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to * `` but can be nested. This allows you to have nested forms, which is very useful when @@ -18407,11 +20077,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { userType: Required!
          - userType = {{userType}}
          - myForm.input.$valid = {{myForm.input.$valid}}
          - myForm.input.$error = {{myForm.input.$error}}
          - myForm.$valid = {{myForm.$valid}}
          - myForm.$error.required = {{!!myForm.$error.required}}
          + userType = {{userType}}
          + myForm.input.$valid = {{myForm.input.$valid}}
          + myForm.input.$error = {{myForm.input.$error}}
          + myForm.$valid = {{myForm.$valid}}
          + myForm.$error.required = {{!!myForm.$error.required}}
          @@ -18446,10 +20116,12 @@ var formDirectiveFactory = function(isNgForm) { name: 'form', restrict: isNgForm ? 'EAC' : 'E', controller: FormController, - compile: function ngFormCompile(formElement) { + compile: function ngFormCompile(formElement, attr) { // Setup initial state of the control formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); + var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false); + return { pre: function ngFormPreLink(scope, formElement, attr, controller) { // if `action` attr is not present on the form, prevent the default action (submission) @@ -18480,23 +20152,21 @@ var formDirectiveFactory = function(isNgForm) { }); } - var parentFormCtrl = controller.$$parentForm, - alias = controller.$name; + var parentFormCtrl = controller.$$parentForm; - if (alias) { - setter(scope, alias, controller, alias); - attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) { - if (alias === newValue) return; - setter(scope, alias, undefined, alias); - alias = newValue; - setter(scope, alias, controller, alias); - parentFormCtrl.$$renameControl(controller, alias); + if (nameAttr) { + setter(scope, controller.$name, controller, controller.$name); + attr.$observe(nameAttr, function(newValue) { + if (controller.$name === newValue) return; + setter(scope, controller.$name, undefined, controller.$name); + parentFormCtrl.$$renameControl(controller, newValue); + setter(scope, controller.$name, controller, controller.$name); }); } formElement.on('$destroy', function() { parentFormCtrl.$removeControl(controller); - if (alias) { - setter(scope, alias, undefined, alias); + if (nameAttr) { + setter(scope, attr[nameAttr], undefined, controller.$name); } extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards }); @@ -18512,12 +20182,13 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); -/* global VALID_CLASS: true, - INVALID_CLASS: true, - PRISTINE_CLASS: true, - DIRTY_CLASS: true, - UNTOUCHED_CLASS: true, - TOUCHED_CLASS: true, +/* global VALID_CLASS: false, + INVALID_CLASS: false, + PRISTINE_CLASS: false, + DIRTY_CLASS: false, + UNTOUCHED_CLASS: false, + TOUCHED_CLASS: false, + $ngModelMinErr: false, */ // Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 @@ -18530,9 +20201,6 @@ var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{ 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 DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; - -var $ngModelMinErr = new minErr('ngModel'); var inputType = { @@ -18560,9 +20228,13 @@ var inputType = { * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object then this is used directly. - * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` - * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
          + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. @@ -18575,19 +20247,24 @@ var inputType = {
          - Single word: - - Required! - - Single word only! - - text = {{text}}
          + +
          + + Required! + + Single word only! +
          + text = {{example.text}}
          myForm.input.$valid = {{myForm.input.$valid}}
          myForm.input.$error = {{myForm.input.$error}}
          myForm.$valid = {{myForm.$valid}}
          @@ -18595,9 +20272,9 @@ var inputType = {
          - var text = element(by.binding('text')); + var text = element(by.binding('example.text')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); + var input = element(by.model('example.text')); it('should initialize to model', function() { expect(text.getText()).toContain('guest'); @@ -18659,18 +20336,22 @@ var inputType = {
          - Pick a date in 2013: - Pick a date in 2013: + - - Required! - - Not a valid date! - value = {{value | date: "yyyy-MM-dd"}}
          +
          + + Required! + + Not a valid date! +
          + value = {{example.value | date: "yyyy-MM-dd"}}
          myForm.input.$valid = {{myForm.input.$valid}}
          myForm.input.$error = {{myForm.input.$error}}
          myForm.$valid = {{myForm.$valid}}
          @@ -18678,9 +20359,9 @@ var inputType = {
          - var value = element(by.binding('value | date: "yyyy-MM-dd"')); + var value = element(by.binding('example.value | date: "yyyy-MM-dd"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('value')); + var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -18750,18 +20431,22 @@ var inputType = {
          - Pick a date between in 2013: - Pick a date between in 2013: + - - Required! - - Not a valid date! - value = {{value | date: "yyyy-MM-ddTHH:mm:ss"}}
          +
          + + Required! + + Not a valid date! +
          + value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}
          myForm.input.$valid = {{myForm.input.$valid}}
          myForm.input.$error = {{myForm.input.$error}}
          myForm.$valid = {{myForm.$valid}}
          @@ -18769,9 +20454,9 @@ var inputType = {
          - var value = element(by.binding('value | date: "yyyy-MM-ddTHH:mm:ss"')); + var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('value')); + var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -18842,18 +20527,22 @@ var inputType = {
          - Pick a between 8am and 5pm: - Pick a between 8am and 5pm: + - - Required! - - Not a valid date! - value = {{value | date: "HH:mm:ss"}}
          +
          + + Required! + + Not a valid date! +
          + value = {{example.value | date: "HH:mm:ss"}}
          myForm.input.$valid = {{myForm.input.$valid}}
          myForm.input.$error = {{myForm.input.$error}}
          myForm.$valid = {{myForm.$valid}}
          @@ -18861,9 +20550,9 @@ var inputType = {
          - var value = element(by.binding('value | date: "HH:mm:ss"')); + var value = element(by.binding('example.value | date: "HH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('value')); + var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -18933,18 +20622,24 @@ var inputType = {
          - Pick a date between in 2013: - - - Required! - - Not a valid date! - value = {{value | date: "yyyy-Www"}}
          + +
          + + Required! + + Not a valid date! +
          + value = {{example.value | date: "yyyy-Www"}}
          myForm.input.$valid = {{myForm.input.$valid}}
          myForm.input.$error = {{myForm.input.$error}}
          myForm.$valid = {{myForm.$valid}}
          @@ -18952,9 +20647,9 @@ var inputType = {
          - var value = element(by.binding('value | date: "yyyy-Www"')); + var value = element(by.binding('example.value | date: "yyyy-Www"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('value')); + var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -19024,18 +20719,22 @@ var inputType = {
          - Pick a month int 2013: - Pick a month in 2013: + - - Required! - - Not a valid month! - value = {{value | date: "yyyy-MM"}}
          +
          + + Required! + + Not a valid month! +
          + value = {{example.value | date: "yyyy-MM"}}
          myForm.input.$valid = {{myForm.input.$valid}}
          myForm.input.$error = {{myForm.input.$error}}
          myForm.$valid = {{myForm.$valid}}
          @@ -19043,9 +20742,9 @@ var inputType = {
          - var value = element(by.binding('value | date: "yyyy-MM"')); + var value = element(by.binding('example.value | date: "yyyy-MM"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('value')); + var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -19089,7 +20788,11 @@ var inputType = { * Text input with number validation and transformation. Sets the `number` validation * error if not a valid number. * - * The model must always be a number, otherwise Angular will throw an error. + *
          + * The model must always be of type `number` otherwise Angular will throw an error. + * Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt} + * error docs for more information and an example of how to convert your model if necessary. + *
          * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -19109,9 +20812,13 @@ var inputType = { * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object then this is used directly. - * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` - * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
          + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @@ -19121,17 +20828,23 @@ var inputType = {
          - Number: - - Required! - - Not valid number! - value = {{value}}
          + +
          + + Required! + + Not valid number! +
          + value = {{example.value}}
          myForm.input.$valid = {{myForm.input.$valid}}
          myForm.input.$error = {{myForm.input.$error}}
          myForm.$valid = {{myForm.$valid}}
          @@ -19139,9 +20852,9 @@ var inputType = {
          - var value = element(by.binding('value')); + var value = element(by.binding('example.value')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('value')); + var input = element(by.model('example.value')); it('should initialize to model', function() { expect(value.getText()).toContain('12'); @@ -19197,9 +20910,13 @@ var inputType = { * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object then this is used directly. - * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` - * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
          + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @@ -19209,16 +20926,22 @@ var inputType = {
          - URL: - - Required! - - Not valid url! - text = {{text}}
          +
          - var text = element(by.binding('text')); + var text = element(by.binding('url.text')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); + var input = element(by.model('url.text')); it('should initialize to model', function() { expect(text.getText()).toContain('http://google.com'); @@ -19286,9 +21009,13 @@ var inputType = { * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object then this is used directly. - * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` - * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
          + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @@ -19298,16 +21025,22 @@ var inputType = {
          - Email: - - Required! - - Not valid email! - text = {{text}}
          + +
          + + Required! + + Not valid email! +
          + text = {{email.text}}
          myForm.input.$valid = {{myForm.input.$valid}}
          myForm.input.$error = {{myForm.input.$error}}
          myForm.$valid = {{myForm.$valid}}
          @@ -19316,9 +21049,9 @@ var inputType = {
          - var text = element(by.binding('text')); + var text = element(by.binding('email.text')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); + var input = element(by.model('email.text')); it('should initialize to model', function() { expect(text.getText()).toContain('me@example.com'); @@ -19365,7 +21098,9 @@ var inputType = {
          - Red
          - Green
          - Blue
          - color = {{color | json}}
          +
          +
          +
          + color = {{color.name | json}}
          Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
          it('should change state', function() { - var color = element(by.binding('color')); + var color = element(by.binding('color.name')); expect(color.getText()).toContain('blue'); - element.all(by.model('color')).get(0).click(); + element.all(by.model('color.name')).get(0).click(); expect(color.getText()).toContain('red'); }); @@ -19416,28 +21160,34 @@ var inputType = {
          - Value1:
          - Value2:
          - value1 = {{value1}}
          - value2 = {{value2}}
          +
          +
          + value1 = {{checkboxModel.value1}}
          + value2 = {{checkboxModel.value2}}
          it('should change state', function() { - var value1 = element(by.binding('value1')); - var value2 = element(by.binding('value2')); + var value1 = element(by.binding('checkboxModel.value1')); + var value2 = element(by.binding('checkboxModel.value2')); expect(value1.getText()).toContain('true'); expect(value2.getText()).toContain('YES'); - element(by.model('value1')).click(); - element(by.model('value2')).click(); + element(by.model('checkboxModel.value1')).click(); + element(by.model('checkboxModel.value2')).click(); expect(value1.getText()).toContain('false'); expect(value2.getText()).toContain('NO'); @@ -19466,7 +21216,6 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { - var placeholder = element[0].placeholder, noevent = {}; var type = lowercase(element[0].type); // In composition mode, users are still inputing intermediate text buffer, @@ -19486,19 +21235,14 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { } var listener = function(ev) { + if (timeout) { + $browser.defer.cancel(timeout); + timeout = null; + } if (composing) return; var value = element.val(), event = ev && ev.type; - // IE (11 and under) seem to emit an 'input' event if the placeholder value changes. - // We don't want to dirty the value when this happens, so we abort here. Unfortunately, - // IE also sends input events for other non-input-related things, (such as focusing on a - // form control), so this change is not entirely enough to solve this. - if (msie && (ev || noevent).type === 'input' && element[0].placeholder !== placeholder) { - placeholder = element[0].placeholder; - return; - } - // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming // If input type is 'password', the value is never trimmed @@ -19521,11 +21265,13 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { } else { var timeout; - var deferListener = function(ev) { + var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { - listener(ev); timeout = null; + if (!input || input.value !== origValue) { + listener(ev); + } }); } }; @@ -19537,7 +21283,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - deferListener(event); + deferListener(event, this, this.value); }); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it @@ -19652,8 +21398,8 @@ function createDateInputType(type, regexp, parseDate, format) { // parser/formatter in the processing chain so that the model // contains some different data format! var parsedDate = parseDate(value, previousDate); - if (timezone === 'UTC') { - parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); + if (timezone) { + parsedDate = convertTimezoneToLocal(parsedDate, timezone); } return parsedDate; } @@ -19666,9 +21412,8 @@ function createDateInputType(type, regexp, parseDate, format) { } if (isValidDate(value)) { previousDate = value; - if (previousDate && timezone === 'UTC') { - var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); - previousDate = new Date(previousDate.getTime() + timezoneOffset); + if (previousDate && timezone) { + previousDate = convertTimezoneToLocal(previousDate, timezone, true); } return $filter('date')(value, format, timezone); } else { @@ -19746,7 +21491,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { return value; }); - if (attr.min || attr.ngMin) { + if (isDefined(attr.min) || attr.ngMin) { var minVal; ctrl.$validators.min = function(value) { return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; @@ -19762,7 +21507,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } - if (attr.max || attr.ngMax) { + if (isDefined(attr.max) || attr.ngMax) { var maxVal; ctrl.$validators.max = function(value) { return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; @@ -19892,9 +21637,15 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
          + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. @@ -19925,9 +21676,15 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
          + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. @@ -19945,26 +21702,36 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
          - User name: - - Required!
          - Last name: - - Too short! - - Too long!
          + +
          + + Required! +
          + +
          + + Too short! + + Too long! +

          user = {{user}}
          - myForm.userName.$valid = {{myForm.userName.$valid}}
          - myForm.userName.$error = {{myForm.userName.$error}}
          - myForm.lastName.$valid = {{myForm.lastName.$valid}}
          - myForm.lastName.$error = {{myForm.lastName.$error}}
          - myForm.$valid = {{myForm.$valid}}
          - myForm.$error.required = {{!!myForm.$error.required}}
          - myForm.$error.minlength = {{!!myForm.$error.minlength}}
          - myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
          + myForm.userName.$valid = {{myForm.userName.$valid}}
          + myForm.userName.$error = {{myForm.userName.$error}}
          + myForm.lastName.$valid = {{myForm.lastName.$valid}}
          + myForm.lastName.$error = {{myForm.lastName.$error}}
          + myForm.$valid = {{myForm.$valid}}
          + myForm.$error.required = {{!!myForm.$error.required}}
          + myForm.$error.minlength = {{!!myForm.$error.minlength}}
          + myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
          @@ -20038,1340 +21805,6 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', }; }]; -var VALID_CLASS = 'ng-valid', - INVALID_CLASS = 'ng-invalid', - PRISTINE_CLASS = 'ng-pristine', - DIRTY_CLASS = 'ng-dirty', - UNTOUCHED_CLASS = 'ng-untouched', - TOUCHED_CLASS = 'ng-touched', - PENDING_CLASS = 'ng-pending'; - -/** - * @ngdoc type - * @name ngModel.NgModelController - * - * @property {string} $viewValue Actual string value in the view. - * @property {*} $modelValue The value in the model that the control is bound to. - * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever - the control reads value from the DOM. The functions are called in array order, each passing - its return value through to the next. The last return value is forwarded to the - {@link ngModel.NgModelController#$validators `$validators`} collection. - -Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue -`$viewValue`}. - -Returning `undefined` from a parser means a parse error occurred. In that case, -no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` -will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} -is set to `true`. The parse error is stored in `ngModel.$error.parse`. - - * - * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever - the model value changes. The functions are called in reverse array order, each passing the value through to the - next. The last return value is used as the actual DOM value. - Used to format / convert values for display in the control. - * ```js - * function formatter(value) { - * if (value) { - * return value.toUpperCase(); - * } - * } - * ngModel.$formatters.push(formatter); - * ``` - * - * @property {Object.} $validators A collection of validators that are applied - * whenever the model value changes. The key value within the object refers to the name of the - * validator while the function refers to the validation operation. The validation operation is - * provided with the model value as an argument and must return a true or false value depending - * on the response of that validation. - * - * ```js - * ngModel.$validators.validCharacters = function(modelValue, viewValue) { - * var value = modelValue || viewValue; - * return /[0-9]+/.test(value) && - * /[a-z]+/.test(value) && - * /[A-Z]+/.test(value) && - * /\W+/.test(value); - * }; - * ``` - * - * @property {Object.} $asyncValidators A collection of validations that are expected to - * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided - * is expected to return a promise when it is run during the model validation process. Once the promise - * is delivered then the validation status will be set to true when fulfilled and false when rejected. - * When the asynchronous validators are triggered, each of the validators will run in parallel and the model - * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator - * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators - * will only run once all synchronous validators have passed. - * - * Please note that if $http is used then it is important that the server returns a success HTTP response code - * in order to fulfill the validation and a status level of `4xx` in order to reject the validation. - * - * ```js - * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { - * var value = modelValue || viewValue; - * - * // Lookup user by username - * return $http.get('/api/users/' + value). - * then(function resolved() { - * //username exists, this means validation fails - * return $q.reject('exists'); - * }, function rejected() { - * //username does not exist, therefore this validation passes - * return true; - * }); - * }; - * ``` - * - * @property {Array.} $viewChangeListeners Array of functions to execute whenever the - * view value has changed. It is called with no arguments, and its return value is ignored. - * This can be used in place of additional $watches against the model value. - * - * @property {Object} $error An object hash with all failing validator ids as keys. - * @property {Object} $pending An object hash with all pending validator ids as keys. - * - * @property {boolean} $untouched True if control has not lost focus yet. - * @property {boolean} $touched True if control has lost focus. - * @property {boolean} $pristine True if user has not interacted with the control yet. - * @property {boolean} $dirty True if user has already interacted with the control. - * @property {boolean} $valid True if there is no error. - * @property {boolean} $invalid True if at least one error on the control. - * @property {string} $name The name attribute of the control. - * - * @description - * - * `NgModelController` provides API for the {@link ngModel `ngModel`} directive. - * The controller contains services for data-binding, validation, CSS updates, and value formatting - * and parsing. It purposefully does not contain any logic which deals with DOM rendering or - * listening to DOM events. - * Such DOM related logic should be provided by other directives which make use of - * `NgModelController` for data-binding to control elements. - * Angular provides this DOM logic for most {@link input `input`} elements. - * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example - * custom control example} that uses `ngModelController` to bind to `contenteditable` elements. - * - * @example - * ### Custom Control Example - * This example shows how to use `NgModelController` with a custom control to achieve - * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) - * collaborate together to achieve the desired result. - * - * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element - * contents be edited in place by the user. This will not work on older browsers. - * - * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} - * module to automatically remove "bad" content like inline event listener (e.g. ``). - * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks - * that content using the `$sce` service. - * - * - - [contenteditable] { - border: 1px solid black; - background-color: white; - min-height: 20px; - } - - .ng-invalid { - border: 1px solid red; - } - - - - angular.module('customControl', ['ngSanitize']). - directive('contenteditable', ['$sce', function($sce) { - return { - restrict: 'A', // only activate on element attribute - require: '?ngModel', // get a hold of NgModelController - link: function(scope, element, attrs, ngModel) { - if (!ngModel) return; // do nothing if no ng-model - - // Specify how UI should be updated - ngModel.$render = function() { - element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); - }; - - // Listen for change events to enable binding - element.on('blur keyup change', function() { - scope.$evalAsync(read); - }); - read(); // initialize - - // Write data to the model - function read() { - var html = element.html(); - // When we clear the content editable the browser leaves a
          behind - // If strip-br attribute is provided then we strip this out - if ( attrs.stripBr && html == '
          ' ) { - html = ''; - } - ngModel.$setViewValue(html); - } - } - }; - }]); -
          - -
          -
          Change me!
          - Required! -
          - -
          -
          - - it('should data-bind and become invalid', function() { - if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { - // SafariDriver can't handle contenteditable - // and Firefox driver can't clear contenteditables very well - return; - } - var contentEditable = element(by.css('[contenteditable]')); - var content = 'Change me!'; - - expect(contentEditable.getText()).toEqual(content); - - contentEditable.clear(); - contentEditable.sendKeys(protractor.Key.BACK_SPACE); - expect(contentEditable.getText()).toEqual(''); - expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); - }); - - *
          - * - * - */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { - this.$viewValue = Number.NaN; - this.$modelValue = Number.NaN; - this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. - this.$validators = {}; - this.$asyncValidators = {}; - this.$parsers = []; - this.$formatters = []; - this.$viewChangeListeners = []; - this.$untouched = true; - this.$touched = false; - this.$pristine = true; - this.$dirty = false; - this.$valid = true; - this.$invalid = false; - this.$error = {}; // keep invalid keys here - this.$$success = {}; // keep valid keys here - this.$pending = undefined; // keep pending keys here - this.$name = $interpolate($attr.name || '', false)($scope); - - - var parsedNgModel = $parse($attr.ngModel), - parsedNgModelAssign = parsedNgModel.assign, - ngModelGet = parsedNgModel, - ngModelSet = parsedNgModelAssign, - pendingDebounce = null, - ctrl = this; - - this.$$setOptions = function(options) { - ctrl.$options = options; - if (options && options.getterSetter) { - var invokeModelGetter = $parse($attr.ngModel + '()'), - invokeModelSetter = $parse($attr.ngModel + '($$$p)'); - - ngModelGet = function($scope) { - var modelValue = parsedNgModel($scope); - if (isFunction(modelValue)) { - modelValue = invokeModelGetter($scope); - } - return modelValue; - }; - ngModelSet = function($scope, newValue) { - if (isFunction(parsedNgModel($scope))) { - invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); - } else { - parsedNgModelAssign($scope, ctrl.$modelValue); - } - }; - } else if (!parsedNgModel.assign) { - throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); - } - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$render - * - * @description - * Called when the view needs to be updated. It is expected that the user of the ng-model - * directive will implement this method. - * - * The `$render()` method is invoked in the following situations: - * - * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last - * committed value then `$render()` is called to update the input control. - * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and - * the `$viewValue` are different to 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 to their previous value. 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. - */ - this.$render = noop; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$isEmpty - * - * @description - * This is called when we need to determine if the value of an input is empty. - * - * For instance, the required directive does this to work out if the input has data or not. - * - * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. - * - * You can override this for input directives whose concept of being empty is different to the - * default. The `checkboxInputType` directive does this because in its case a value of `false` - * implies empty. - * - * @param {*} value The value of the input to check for emptiness. - * @returns {boolean} True if `value` is "empty". - */ - this.$isEmpty = function(value) { - return isUndefined(value) || value === '' || value === null || value !== value; - }; - - var parentForm = $element.inheritedData('$formController') || nullFormCtrl, - currentValidationRunId = 0; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setValidity - * - * @description - * Change the validity state, and notify the form. - * - * This method can be called within $parsers/$formatters or a custom validation implementation. - * However, in most cases it should be sufficient to use the `ngModel.$validators` and - * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. - * - * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned - * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` - * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. - * The `validationErrorKey` should be in camelCase and will get converted into dash-case - * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` - * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), - * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. - * Skipped is used by Angular when validators do not run because of parse errors and - * when `$asyncValidators` do not run because any of the `$validators` failed. - */ - addSetValidityMethod({ - ctrl: this, - $element: $element, - set: function(object, property) { - object[property] = true; - }, - unset: function(object, property) { - delete object[property]; - }, - parentForm: parentForm, - $animate: $animate - }); - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setPristine - * - * @description - * Sets the control to its pristine state. - * - * This method can be called to remove the `ng-dirty` class and set the control to its pristine - * state (`ng-pristine` class). A model is considered to be pristine when the control - * has not been changed from when first compiled. - */ - this.$setPristine = function() { - ctrl.$dirty = false; - ctrl.$pristine = true; - $animate.removeClass($element, DIRTY_CLASS); - $animate.addClass($element, PRISTINE_CLASS); - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setDirty - * - * @description - * Sets the control to its dirty state. - * - * This method can be called to remove the `ng-pristine` class and set the control to its dirty - * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed - * from when first compiled. - */ - this.$setDirty = function() { - ctrl.$dirty = true; - ctrl.$pristine = false; - $animate.removeClass($element, PRISTINE_CLASS); - $animate.addClass($element, DIRTY_CLASS); - parentForm.$setDirty(); - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setUntouched - * - * @description - * Sets the control to its untouched state. - * - * This method can be called to remove the `ng-touched` class and set the control to its - * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched - * by default, however this function can be used to restore that state if the model has - * already been touched by the user. - */ - this.$setUntouched = function() { - ctrl.$touched = false; - ctrl.$untouched = true; - $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setTouched - * - * @description - * Sets the control to its touched state. - * - * This method can be called to remove the `ng-untouched` class and set the control to its - * touched state (`ng-touched` class). A model is considered to be touched when the user has - * first focused the control element and then shifted focus away from the control (blur event). - */ - this.$setTouched = function() { - ctrl.$touched = true; - ctrl.$untouched = false; - $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$rollbackViewValue - * - * @description - * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, - * which may be caused by a pending debounced event or because the input is waiting for a some - * future event. - * - * If you have an input that uses `ng-model-options` to set up debounced events or events such - * as blur you can have a situation where there is a period when the `$viewValue` - * is out of synch with the ngModel's `$modelValue`. - * - * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue` - * programmatically before these debounced/future events have resolved/occurred, because Angular's - * dirty checking mechanism is not able to tell whether the model has actually changed or not. - * - * The `$rollbackViewValue()` method should be called before programmatically changing the model of an - * input which may have such events pending. This is important in order to make sure that the - * input field will be updated with the new model value and any pending operations are cancelled. - * - * - * - * angular.module('cancel-update-example', []) - * - * .controller('CancelUpdateController', ['$scope', function($scope) { - * $scope.resetWithCancel = function(e) { - * if (e.keyCode == 27) { - * $scope.myForm.myInput1.$rollbackViewValue(); - * $scope.myValue = ''; - * } - * }; - * $scope.resetWithoutCancel = function(e) { - * if (e.keyCode == 27) { - * $scope.myValue = ''; - * } - * }; - * }]); - * - * - *
          - *

          Try typing something in each input. See that the model only updates when you - * blur off the input. - *

          - *

          Now see what happens if you start typing then press the Escape key

          - * - *
          - *

          With $rollbackViewValue()

          - *
          - * myValue: "{{ myValue }}" - * - *

          Without $rollbackViewValue()

          - *
          - * myValue: "{{ myValue }}" - *
          - *
          - *
          - *
          - */ - this.$rollbackViewValue = function() { - $timeout.cancel(pendingDebounce); - ctrl.$viewValue = ctrl.$$lastCommittedViewValue; - ctrl.$render(); - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$validate - * - * @description - * Runs each of the registered validators (first synchronous validators and then - * asynchronous validators). - * If the validity changes to invalid, the model will be set to `undefined`, - * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`. - * If the validity changes to valid, it will set the model to the last available valid - * modelValue, i.e. either the last parsed value or the last value set from the scope. - */ - this.$validate = function() { - // ignore $validate before model is initialized - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { - return; - } - - var viewValue = ctrl.$$lastCommittedViewValue; - // Note: we use the $$rawModelValue as $modelValue might have been - // set to undefined during a view -> model update that found validation - // errors. We can't parse the view here, since that could change - // the model although neither viewValue nor the model on the scope changed - var modelValue = ctrl.$$rawModelValue; - - // Check if the there's a parse error, so we don't unset it accidentially - var parserName = ctrl.$$parserName || 'parse'; - var parserValid = ctrl.$error[parserName] ? false : undefined; - - var prevValid = ctrl.$valid; - var prevModelValue = ctrl.$modelValue; - - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; - - ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) { - // If there was no change in validity, don't update the model - // This prevents changing an invalid modelValue to undefined - if (!allowInvalid && prevValid !== allValid) { - // Note: Don't check ctrl.$valid here, as we could have - // external validators (e.g. calculated on the server), - // that just call $setValidity and need the model value - // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; - - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); - } - } - }); - - }; - - this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) { - currentValidationRunId++; - var localValidationRunId = currentValidationRunId; - - // check parser error - if (!processParseErrors(parseValid)) { - validationDone(false); - return; - } - if (!processSyncValidators()) { - validationDone(false); - return; - } - processAsyncValidators(); - - function processParseErrors(parseValid) { - var errorKey = ctrl.$$parserName || 'parse'; - if (parseValid === undefined) { - setValidity(errorKey, null); - } else { - setValidity(errorKey, parseValid); - if (!parseValid) { - forEach(ctrl.$validators, function(v, name) { - setValidity(name, null); - }); - forEach(ctrl.$asyncValidators, function(v, name) { - setValidity(name, null); - }); - return false; - } - } - return true; - } - - function processSyncValidators() { - var syncValidatorsValid = true; - forEach(ctrl.$validators, function(validator, name) { - var result = validator(modelValue, viewValue); - syncValidatorsValid = syncValidatorsValid && result; - setValidity(name, result); - }); - if (!syncValidatorsValid) { - forEach(ctrl.$asyncValidators, function(v, name) { - setValidity(name, null); - }); - return false; - } - return true; - } - - function processAsyncValidators() { - var validatorPromises = []; - var allValid = true; - forEach(ctrl.$asyncValidators, function(validator, name) { - var promise = validator(modelValue, viewValue); - if (!isPromiseLike(promise)) { - throw $ngModelMinErr("$asyncValidators", - "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); - } - setValidity(name, undefined); - validatorPromises.push(promise.then(function() { - setValidity(name, true); - }, function(error) { - allValid = false; - setValidity(name, false); - })); - }); - if (!validatorPromises.length) { - validationDone(true); - } else { - $q.all(validatorPromises).then(function() { - validationDone(allValid); - }, noop); - } - } - - function setValidity(name, isValid) { - if (localValidationRunId === currentValidationRunId) { - ctrl.$setValidity(name, isValid); - } - } - - function validationDone(allValid) { - if (localValidationRunId === currentValidationRunId) { - - doneCallback(allValid); - } - } - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$commitViewValue - * - * @description - * Commit a pending update to the `$modelValue`. - * - * Updates may be pending by a debounced event or because the input is waiting for a some future - * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` - * usually handles calling this in response to input events. - */ - this.$commitViewValue = function() { - var viewValue = ctrl.$viewValue; - - $timeout.cancel(pendingDebounce); - - // If the view value has not changed then we should just exit, except in the case where there is - // a native validator on the element. In this case the validation state may have changed even though - // the viewValue has stayed empty. - if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { - return; - } - ctrl.$$lastCommittedViewValue = viewValue; - - // change to dirty - if (ctrl.$pristine) { - this.$setDirty(); - } - this.$$parseAndValidate(); - }; - - this.$$parseAndValidate = function() { - var viewValue = ctrl.$$lastCommittedViewValue; - var modelValue = viewValue; - var parserValid = isUndefined(modelValue) ? undefined : true; - - if (parserValid) { - for (var i = 0; i < ctrl.$parsers.length; i++) { - modelValue = ctrl.$parsers[i](modelValue); - if (isUndefined(modelValue)) { - parserValid = false; - break; - } - } - } - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { - // ctrl.$modelValue has not been touched yet... - ctrl.$modelValue = ngModelGet($scope); - } - var prevModelValue = ctrl.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; - ctrl.$$rawModelValue = modelValue; - if (allowInvalid) { - ctrl.$modelValue = modelValue; - writeToModelIfNeeded(); - } - ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) { - if (!allowInvalid) { - // Note: Don't check ctrl.$valid here, as we could have - // external validators (e.g. calculated on the server), - // that just call $setValidity and need the model value - // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; - writeToModelIfNeeded(); - } - }); - - function writeToModelIfNeeded() { - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); - } - } - }; - - this.$$writeModelToScope = function() { - ngModelSet($scope, ctrl.$modelValue); - forEach(ctrl.$viewChangeListeners, function(listener) { - try { - listener(); - } catch (e) { - $exceptionHandler(e); - } - }); - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setViewValue - * - * @description - * Update the view value. - * - * This method should be called when an input directive want to change the view value; typically, - * this is done from within a DOM event handler. - * - * For example {@link ng.directive:input input} calls it when the value of the input changes and - * {@link ng.directive:select select} calls it when an option is selected. - * - * If the new `value` is an object (rather than a string or a number), we should make a copy of the - * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep - * watch of objects, it only looks for a change of identity. If you only change the property of - * the object then ngModel will not realise that the object has changed and will not invoke the - * `$parsers` and `$validators` pipelines. - * - * For this reason, you should not change properties of the copy once it has been passed to - * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly. - * - * When this method is called, the new `value` will be staged for committing through the `$parsers` - * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged - * value sent directly for processing, finally to be applied to `$modelValue` and then the - * **expression** specified in the `ng-model` attribute. - * - * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. - * - * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` - * and the `default` trigger is not listed, all those actions will remain pending until one of the - * `updateOn` events is triggered on the DOM element. - * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions} - * directive is used with a custom debounce for this particular event. - * - * Note that calling this function does not trigger a `$digest`. - * - * @param {string} value Value from the view. - * @param {string} trigger Event that triggered the update. - */ - this.$setViewValue = function(value, trigger) { - ctrl.$viewValue = value; - if (!ctrl.$options || ctrl.$options.updateOnDefault) { - ctrl.$$debounceViewValueCommit(trigger); - } - }; - - this.$$debounceViewValueCommit = function(trigger) { - var debounceDelay = 0, - options = ctrl.$options, - debounce; - - if (options && isDefined(options.debounce)) { - debounce = options.debounce; - if (isNumber(debounce)) { - debounceDelay = debounce; - } else if (isNumber(debounce[trigger])) { - debounceDelay = debounce[trigger]; - } else if (isNumber(debounce['default'])) { - debounceDelay = debounce['default']; - } - } - - $timeout.cancel(pendingDebounce); - if (debounceDelay) { - pendingDebounce = $timeout(function() { - ctrl.$commitViewValue(); - }, debounceDelay); - } else if ($rootScope.$$phase) { - ctrl.$commitViewValue(); - } else { - $scope.$apply(function() { - ctrl.$commitViewValue(); - }); - } - }; - - // model -> value - // Note: we cannot use a normal scope.$watch as we want to detect the following: - // 1. scope value is 'a' - // 2. user enters 'b' - // 3. ng-change kicks in and reverts scope value to 'a' - // -> scope value did not change since the last digest as - // ng-change executes in apply phase - // 4. view should be changed back to 'a' - $scope.$watch(function ngModelWatch() { - var modelValue = ngModelGet($scope); - - // if scope model value and ngModel value are out of sync - // TODO(perf): why not move this to the action fn? - if (modelValue !== ctrl.$modelValue) { - ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; - - var formatters = ctrl.$formatters, - idx = formatters.length; - - var viewValue = modelValue; - while (idx--) { - viewValue = formatters[idx](viewValue); - } - if (ctrl.$viewValue !== viewValue) { - ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; - ctrl.$render(); - - ctrl.$$runValidators(undefined, modelValue, viewValue, noop); - } - } - - return modelValue; - }); -}]; - - -/** - * @ngdoc directive - * @name ngModel - * - * @element input - * @priority 1 - * - * @description - * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a - * property on the scope using {@link ngModel.NgModelController NgModelController}, - * which is created and exposed by this directive. - * - * `ngModel` is responsible for: - * - * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` - * require. - * - Providing validation behavior (i.e. required, number, email, url). - * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations. - * - Registering the control with its parent {@link ng.directive:form form}. - * - * Note: `ngModel` will try to bind to the property given by evaluating the expression on the - * current scope. If the property doesn't already exist on this scope, it will be created - * implicitly and added to the scope. - * - * For best practices on using `ngModel`, see: - * - * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes) - * - * For basic examples, how to use `ngModel`, see: - * - * - {@link ng.directive:input input} - * - {@link input[text] text} - * - {@link input[checkbox] checkbox} - * - {@link input[radio] radio} - * - {@link input[number] number} - * - {@link input[email] email} - * - {@link input[url] url} - * - {@link input[date] date} - * - {@link input[datetime-local] datetime-local} - * - {@link input[time] time} - * - {@link input[month] month} - * - {@link input[week] week} - * - {@link ng.directive:select select} - * - {@link ng.directive:textarea textarea} - * - * # CSS classes - * The following CSS classes are added and removed on the associated input/select/textarea element - * depending on the validity of the model. - * - * - `ng-valid`: the model is valid - * - `ng-invalid`: the model is invalid - * - `ng-valid-[key]`: for each valid key added by `$setValidity` - * - `ng-invalid-[key]`: for each invalid key added by `$setValidity` - * - `ng-pristine`: the control hasn't been interacted with yet - * - `ng-dirty`: the control has been interacted with - * - `ng-touched`: the control has been blurred - * - `ng-untouched`: the control hasn't been blurred - * - `ng-pending`: any `$asyncValidators` are unfulfilled - * - * Keep in mind that ngAnimate can detect each of these classes when added and removed. - * - * ## Animation Hooks - * - * Animations within models are triggered when any of the associated CSS classes are added and removed - * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, - * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. - * The animations that are triggered within ngModel are similar to how they work in ngClass and - * animations can be hooked into using CSS transitions, keyframes as well as JS animations. - * - * The following example shows a simple way to utilize CSS transitions to style an input element - * that has been rendered as invalid after it has been validated: - * - *
          - * //be sure to include ngAnimate as a module to hook into more
          - * //advanced animations
          - * .my-input {
          - *   transition:0.5s linear all;
          - *   background: white;
          - * }
          - * .my-input.ng-invalid {
          - *   background: red;
          - *   color:white;
          - * }
          - * 
          - * - * @example - * - - - - Update input to see transitions when valid/invalid. - Integer is a valid value. -
          - -
          -
          - *
          - * - * ## Binding to a getter/setter - * - * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a - * function that returns a representation of the model when called with zero arguments, and sets - * the internal state of a model when called with an argument. It's sometimes useful to use this - * for models that have an internal representation that's different than what the model exposes - * to the view. - * - *
          - * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more - * frequently than other parts of your code. - *
          - * - * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that - * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to - * a `
          `, which will enable this behavior for all ``s within it. See - * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. - * - * The following example shows how to use `ngModel` with a getter/setter: - * - * @example - * - -
          - - Name: - - -
          user.name = 
          -
          -
          - - angular.module('getterSetterExample', []) - .controller('ExampleController', ['$scope', function($scope) { - var _name = 'Brian'; - $scope.user = { - name: function(newName) { - if (angular.isDefined(newName)) { - _name = newName; - } - return _name; - } - }; - }]); - - *
          - */ -var ngModelDirective = ['$rootScope', function($rootScope) { - return { - restrict: 'A', - require: ['ngModel', '^?form', '^?ngModelOptions'], - controller: NgModelController, - // Prelink needs to run before any input directive - // so that we can set the NgModelOptions in NgModelController - // before anyone else uses it. - priority: 1, - compile: function ngModelCompile(element) { - // Setup initial state of the control - element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS); - - return { - pre: function ngModelPreLink(scope, element, attr, ctrls) { - var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; - - modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); - - // notify others, especially parent forms - formCtrl.$addControl(modelCtrl); - - attr.$observe('name', function(newValue) { - if (modelCtrl.$name !== newValue) { - formCtrl.$$renameControl(modelCtrl, newValue); - } - }); - - scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); - }); - }, - post: function ngModelPostLink(scope, element, attr, ctrls) { - var modelCtrl = ctrls[0]; - if (modelCtrl.$options && modelCtrl.$options.updateOn) { - element.on(modelCtrl.$options.updateOn, function(ev) { - modelCtrl.$$debounceViewValueCommit(ev && ev.type); - }); - } - - element.on('blur', function(ev) { - if (modelCtrl.$touched) return; - - if ($rootScope.$$phase) { - scope.$evalAsync(modelCtrl.$setTouched); - } else { - scope.$apply(modelCtrl.$setTouched); - } - }); - } - }; - } - }; -}]; - - -/** - * @ngdoc directive - * @name ngChange - * - * @description - * Evaluate the given expression when the user changes the input. - * The expression is evaluated immediately, unlike the JavaScript onchange event - * which only triggers at the end of a change (usually, when the user leaves the - * form element or presses the return key). - * - * The `ngChange` expression is only evaluated when a change in the input value causes - * a new value to be committed to the model. - * - * It will not be evaluated: - * * if the value returned from the `$parsers` transformation pipeline has not changed - * * if the input has continued to be invalid since the model will stay `null` - * * if the model is changed programmatically and not by a change to the input value - * - * - * Note, this directive requires `ngModel` to be present. - * - * @element input - * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change - * in input value. - * - * @example - * - * - * - *
          - * - * - *
          - * debug = {{confirmed}}
          - * counter = {{counter}}
          - *
          - *
          - * - * var counter = element(by.binding('counter')); - * var debug = element(by.binding('confirmed')); - * - * it('should evaluate the expression if changing from view', function() { - * expect(counter.getText()).toContain('0'); - * - * element(by.id('ng-change-example1')).click(); - * - * expect(counter.getText()).toContain('1'); - * expect(debug.getText()).toContain('true'); - * }); - * - * it('should not evaluate the expression if changing from model', function() { - * element(by.id('ng-change-example2')).click(); - - * expect(counter.getText()).toContain('0'); - * expect(debug.getText()).toContain('true'); - * }); - * - *
          - */ -var ngChangeDirective = valueFn({ - restrict: 'A', - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - ctrl.$viewChangeListeners.push(function() { - scope.$eval(attr.ngChange); - }); - } -}); - - -var requiredDirective = function() { - return { - restrict: 'A', - require: '?ngModel', - link: function(scope, elm, attr, ctrl) { - if (!ctrl) return; - attr.required = true; // force truthy in case we are on non input element - - ctrl.$validators.required = function(modelValue, viewValue) { - return !attr.required || !ctrl.$isEmpty(viewValue); - }; - - attr.$observe('required', function() { - ctrl.$validate(); - }); - } - }; -}; - - -var patternDirective = function() { - return { - restrict: 'A', - require: '?ngModel', - link: function(scope, elm, attr, ctrl) { - if (!ctrl) return; - - var regexp, patternExp = attr.ngPattern || attr.pattern; - attr.$observe('pattern', function(regex) { - if (isString(regex) && regex.length > 0) { - regex = new RegExp('^' + regex + '$'); - } - - if (regex && !regex.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, - regex, startingTag(elm)); - } - - regexp = regex || undefined; - ctrl.$validate(); - }); - - ctrl.$validators.pattern = function(value) { - return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); - }; - } - }; -}; - - -var maxlengthDirective = function() { - return { - restrict: 'A', - require: '?ngModel', - link: function(scope, elm, attr, ctrl) { - if (!ctrl) return; - - var maxlength = -1; - attr.$observe('maxlength', function(value) { - var intVal = int(value); - maxlength = isNaN(intVal) ? -1 : intVal; - ctrl.$validate(); - }); - ctrl.$validators.maxlength = function(modelValue, viewValue) { - return (maxlength < 0) || ctrl.$isEmpty(modelValue) || (viewValue.length <= maxlength); - }; - } - }; -}; - -var minlengthDirective = function() { - return { - restrict: 'A', - require: '?ngModel', - link: function(scope, elm, attr, ctrl) { - if (!ctrl) return; - - var minlength = 0; - attr.$observe('minlength', function(value) { - minlength = int(value) || 0; - ctrl.$validate(); - }); - ctrl.$validators.minlength = function(modelValue, viewValue) { - return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength; - }; - } - }; -}; - - -/** - * @ngdoc directive - * @name ngList - * - * @description - * Text input that converts between a delimited string and an array of strings. The default - * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom - * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. - * - * The behaviour of the directive is affected by the use of the `ngTrim` attribute. - * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each - * list item is respected. This implies that the user of the directive is responsible for - * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a - * tab or newline character. - * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected - * when joining the list items back together) and whitespace around each list item is stripped - * before it is added to the model. - * - * ### Example with Validation - * - * - * - * angular.module('listExample', []) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.names = ['morpheus', 'neo', 'trinity']; - * }]); - * - * - *
          - * List: - * - * Required! - *
          - * names = {{names}}
          - * myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
          - * myForm.namesInput.$error = {{myForm.namesInput.$error}}
          - * myForm.$valid = {{myForm.$valid}}
          - * myForm.$error.required = {{!!myForm.$error.required}}
          - *
          - *
          - * - * var listInput = element(by.model('names')); - * var names = element(by.exactBinding('names')); - * var valid = element(by.binding('myForm.namesInput.$valid')); - * var error = element(by.css('span.error')); - * - * it('should initialize to model', function() { - * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); - * expect(valid.getText()).toContain('true'); - * expect(error.getCssValue('display')).toBe('none'); - * }); - * - * it('should be invalid if empty', function() { - * listInput.clear(); - * listInput.sendKeys(''); - * - * expect(names.getText()).toContain(''); - * expect(valid.getText()).toContain('false'); - * expect(error.getCssValue('display')).not.toBe('none'); - * }); - * - *
          - * - * ### Example - splitting on whitespace - * - * - * - *
          {{ list | json }}
          - *
          - * - * it("should split the text by newlines", function() { - * var listInput = element(by.model('list')); - * var output = element(by.binding('list | json')); - * listInput.sendKeys('abc\ndef\nghi'); - * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); - * }); - * - *
          - * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. - */ -var ngListDirective = function() { - return { - restrict: 'A', - priority: 100, - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - // We want to control whitespace trimming so we use this convoluted approach - // to access the ngList attribute, which doesn't pre-trim the attribute - var ngList = element.attr(attr.$attr.ngList) || ', '; - var trimValues = attr.ngTrim !== 'false'; - var separator = trimValues ? trim(ngList) : ngList; - - var parse = function(viewValue) { - // If the viewValue is invalid (say required but empty) it will be `undefined` - if (isUndefined(viewValue)) return; - - var list = []; - - if (viewValue) { - forEach(viewValue.split(separator), function(value) { - if (value) list.push(trimValues ? trim(value) : value); - }); - } - - return list; - }; - - ctrl.$parsers.push(parse); - ctrl.$formatters.push(function(value) { - if (isArray(value)) { - return value.join(ngList); - } - - return undefined; - }); - - // Override the standard $isEmpty because an empty array means the input is empty. - ctrl.$isEmpty = function(value) { - return !value || !value.length; - }; - } - }; -}; var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; @@ -21452,281 +21885,6 @@ var ngValueDirective = function() { }; }; -/** - * @ngdoc directive - * @name ngModelOptions - * - * @description - * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of - * events that will trigger a model update and/or a debouncing delay so that the actual update only - * takes place when a timer expires; this timer will be reset after another change takes place. - * - * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might - * be different than the value in the actual model. This means that if you update the model you - * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in - * order to make sure it is synchronized with the model and that any debounced action is canceled. - * - * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`} - * method is by making sure the input is placed inside a form that has a `name` attribute. This is - * important because `form` controllers are published to the related scope under the name in their - * `name` attribute. - * - * Any pending changes will take place immediately when an enclosing form is submitted via the - * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` - * to have access to the updated model. - * - * `ngModelOptions` has an effect on the element it's declared on and its descendants. - * - * @param {Object} ngModelOptions options to apply to the current model. Valid keys are: - * - `updateOn`: string specifying which event should the input be bound to. You can set several - * events using an space delimited list. There is a special event called `default` that - * matches the default events belonging of the control. - * - `debounce`: integer value which contains the debounce model update value in milliseconds. A - * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a - * custom value for each event. For example: - * `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"` - * - `allowInvalid`: boolean value which indicates that the model can be set with values that did - * not validate correctly instead of the default behavior of setting the model to undefined. - * - `getterSetter`: boolean value which determines whether or not to treat functions bound to - `ngModel` as getters/setters. - * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for - * ``, ``, ... . Right now, the only supported value is `'UTC'`, - * otherwise the default timezone of the browser will be used. - * - * @example - - The following example shows how to override immediate updates. Changes on the inputs within the - form will update the model only when the control loses focus (blur event). If `escape` key is - pressed while the input field is focused, the value is reset to the value in the current model. - - - -
          -
          - Name: -
          - - Other data: -
          -
          -
          user.name = 
          -
          -
          - - angular.module('optionsExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.user = { name: 'say', data: '' }; - - $scope.cancel = function(e) { - if (e.keyCode == 27) { - $scope.userForm.userName.$rollbackViewValue(); - } - }; - }]); - - - var model = element(by.binding('user.name')); - var input = element(by.model('user.name')); - var other = element(by.model('user.data')); - - it('should allow custom events', function() { - input.sendKeys(' hello'); - input.click(); - expect(model.getText()).toEqual('say'); - other.click(); - expect(model.getText()).toEqual('say hello'); - }); - - it('should $rollbackViewValue when model changes', function() { - input.sendKeys(' hello'); - expect(input.getAttribute('value')).toEqual('say hello'); - input.sendKeys(protractor.Key.ESCAPE); - expect(input.getAttribute('value')).toEqual('say'); - other.click(); - expect(model.getText()).toEqual('say'); - }); - -
          - - This one shows how to debounce model changes. Model will be updated only 1 sec after last change. - If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. - - - -
          -
          - Name: - -
          -
          -
          user.name = 
          -
          -
          - - angular.module('optionsExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.user = { name: 'say' }; - }]); - -
          - - This one shows how to bind to getter/setters: - - - -
          -
          - Name: - -
          -
          user.name = 
          -
          -
          - - angular.module('getterSetterExample', []) - .controller('ExampleController', ['$scope', function($scope) { - var _name = 'Brian'; - $scope.user = { - name: function(newName) { - return angular.isDefined(newName) ? (_name = newName) : _name; - } - }; - }]); - -
          - */ -var ngModelOptionsDirective = function() { - return { - restrict: 'A', - controller: ['$scope', '$attrs', function($scope, $attrs) { - var that = this; - this.$options = $scope.$eval($attrs.ngModelOptions); - // Allow adding/overriding bound events - if (this.$options.updateOn !== undefined) { - this.$options.updateOnDefault = false; - // extract "default" pseudo-event from list of events that can trigger a model update - this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() { - that.$options.updateOnDefault = true; - return ' '; - })); - } else { - this.$options.updateOnDefault = true; - } - }] - }; -}; - -// helper methods -function addSetValidityMethod(context) { - var ctrl = context.ctrl, - $element = context.$element, - classCache = {}, - set = context.set, - unset = context.unset, - parentForm = context.parentForm, - $animate = context.$animate; - - classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); - - ctrl.$setValidity = setValidity; - - function setValidity(validationErrorKey, state, options) { - if (state === undefined) { - createAndSet('$pending', validationErrorKey, options); - } else { - unsetAndCleanup('$pending', validationErrorKey, options); - } - if (!isBoolean(state)) { - unset(ctrl.$error, validationErrorKey, options); - unset(ctrl.$$success, validationErrorKey, options); - } else { - if (state) { - unset(ctrl.$error, validationErrorKey, options); - set(ctrl.$$success, validationErrorKey, options); - } else { - set(ctrl.$error, validationErrorKey, options); - unset(ctrl.$$success, validationErrorKey, options); - } - } - if (ctrl.$pending) { - cachedToggleClass(PENDING_CLASS, true); - ctrl.$valid = ctrl.$invalid = undefined; - toggleValidationCss('', null); - } else { - cachedToggleClass(PENDING_CLASS, false); - ctrl.$valid = isObjectEmpty(ctrl.$error); - ctrl.$invalid = !ctrl.$valid; - toggleValidationCss('', ctrl.$valid); - } - - // re-read the state as the set/unset methods could have - // combined state in ctrl.$error[validationError] (used for forms), - // where setting/unsetting only increments/decrements the value, - // and does not replace it. - var combinedState; - if (ctrl.$pending && ctrl.$pending[validationErrorKey]) { - combinedState = undefined; - } else if (ctrl.$error[validationErrorKey]) { - combinedState = false; - } else if (ctrl.$$success[validationErrorKey]) { - combinedState = true; - } else { - combinedState = null; - } - toggleValidationCss(validationErrorKey, combinedState); - parentForm.$setValidity(validationErrorKey, combinedState, ctrl); - } - - function createAndSet(name, value, options) { - if (!ctrl[name]) { - ctrl[name] = {}; - } - set(ctrl[name], value, options); - } - - function unsetAndCleanup(name, value, options) { - if (ctrl[name]) { - unset(ctrl[name], value, options); - } - if (isObjectEmpty(ctrl[name])) { - ctrl[name] = undefined; - } - } - - function cachedToggleClass(className, switchValue) { - if (switchValue && !classCache[className]) { - $animate.addClass($element, className); - classCache[className] = true; - } else if (!switchValue && classCache[className]) { - $animate.removeClass($element, className); - classCache[className] = false; - } - } - - function toggleValidationCss(validationErrorKey, isValid) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - - cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); - cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); - } -} - -function isObjectEmpty(obj) { - if (obj) { - for (var prop in obj) { - return false; - } - } - return true; -} - /** * @ngdoc directive * @name ngBind @@ -21762,7 +21920,7 @@ function isObjectEmpty(obj) { }]);
          - Enter name:
          +
          Hello !
          @@ -21823,8 +21981,8 @@ var ngBindDirective = ['$compile', function($compile) { }]);
          - Salutation:
          - Name:
          +
          +
          
                  
          @@ -21933,6 +22091,83 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, }; }]; +/** + * @ngdoc directive + * @name ngChange + * + * @description + * Evaluate the given expression when the user changes the input. + * The expression is evaluated immediately, unlike the JavaScript onchange event + * which only triggers at the end of a change (usually, when the user leaves the + * form element or presses the return key). + * + * The `ngChange` expression is only evaluated when a change in the input value causes + * a new value to be committed to the model. + * + * It will not be evaluated: + * * if the value returned from the `$parsers` transformation pipeline has not changed + * * if the input has continued to be invalid since the model will stay `null` + * * if the model is changed programmatically and not by a change to the input value + * + * + * Note, this directive requires `ngModel` to be present. + * + * @element input + * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change + * in input value. + * + * @example + * + * + * + *
          + * + * + *
          + * debug = {{confirmed}}
          + * counter = {{counter}}
          + *
          + *
          + * + * var counter = element(by.binding('counter')); + * var debug = element(by.binding('confirmed')); + * + * it('should evaluate the expression if changing from view', function() { + * expect(counter.getText()).toContain('0'); + * + * element(by.id('ng-change-example1')).click(); + * + * expect(counter.getText()).toContain('1'); + * expect(debug.getText()).toContain('true'); + * }); + * + * it('should not evaluate the expression if changing from model', function() { + * element(by.id('ng-change-example2')).click(); + + * expect(counter.getText()).toContain('0'); + * expect(debug.getText()).toContain('true'); + * }); + * + *
          + */ +var ngChangeDirective = valueFn({ + restrict: 'A', + require: 'ngModel', + link: function(scope, element, attr, ctrl) { + ctrl.$viewChangeListeners.push(function() { + scope.$eval(attr.ngChange); + }); + } +}); + function classDirective(name, selector) { name = 'ngClass' + name; return ['$animate', function($animate) { @@ -21972,7 +22207,9 @@ function classDirective(name, selector) { } function digestClassCounts(classes, count) { - var classCounts = element.data('$classCounts') || {}; + // Use createMap() to prevent class assumptions involving property + // names in Object.prototype + var classCounts = element.data('$classCounts') || createMap(); var classesToUpdate = []; forEach(classes, function(className) { if (count > 0 || classCounts[className]) { @@ -22029,12 +22266,15 @@ function classDirective(name, selector) { } function arrayClasses(classVal) { + var classes = []; if (isArray(classVal)) { - return classVal; + forEach(classVal, function(v) { + classes = classes.concat(arrayClasses(v)); + }); + return classes; } else if (isString(classVal)) { return classVal.split(' '); } else if (isObject(classVal)) { - var classes = []; forEach(classVal, function(v, k) { if (v) { classes = classes.concat(k.split(' ')); @@ -22062,20 +22302,23 @@ function classDirective(name, selector) { * 1. If the expression evaluates to a string, the string should be one or more space-delimited class * names. * - * 2. If the expression evaluates to an array, each element of the array should be a string that is - * one or more space-delimited class names. - * - * 3. If the expression evaluates to an object, then for each key-value pair of the + * 2. If the expression evaluates to an object, then for each key-value pair of the * object with a truthy value the corresponding key is used as a class name. * + * 3. If the expression evaluates to an array, each element of the array should either be a string as in + * type 1 or an object as in type 2. This means that you can mix strings and objects together in an array + * to give you more control over what CSS classes appear. See the code below for an example of this. + * + * * The directive won't add duplicate classes if a particular class was already set. * - * When the expression changes, the previously added classes are removed and only then the - * new classes are added. + * When the expression changes, the previously added classes are removed and only then are the + * new classes added. * * @animations - * add - happens just before the class is applied to the element - * remove - happens just before the class is removed from the element + * **add** - happens just before the class is applied to the elements + * + * **remove** - happens just before the class is removed from the element * * @element ANY * @param {expression} ngClass {@link guide/expression Expression} to eval. The result @@ -22088,21 +22331,38 @@ function classDirective(name, selector) {

          Map Syntax Example

          - deleted (apply "strike" class)
          - important (apply "bold" class)
          - error (apply "red" class) +
          +
          +

          Using String Syntax

          - +

          Using Array Syntax

          -
          -
          -
          +
          +
          +
          +
          +

          Using Array and Map Syntax

          +
          +
          .strike { - text-decoration: line-through; + text-decoration: line-through; } .bold { font-weight: bold; @@ -22110,6 +22370,9 @@ function classDirective(name, selector) { .red { color: red; } + .orange { + color: orange; + } var ps = element.all(by.css('p')); @@ -22134,11 +22397,18 @@ function classDirective(name, selector) { }); it('array example should have 3 classes', function() { - expect(ps.last().getAttribute('class')).toBe(''); + expect(ps.get(2).getAttribute('class')).toBe(''); element(by.model('style1')).sendKeys('bold'); element(by.model('style2')).sendKeys('strike'); element(by.model('style3')).sendKeys('red'); - expect(ps.last().getAttribute('class')).toBe('bold strike red'); + expect(ps.get(2).getAttribute('class')).toBe('bold strike red'); + }); + + it('array with map example should have 2 classes', function() { + expect(ps.last().getAttribute('class')).toBe(''); + element(by.model('style4')).sendKeys('bold'); + element(by.model('warning')).click(); + expect(ps.last().getAttribute('class')).toBe('bold orange'); });
          @@ -22188,8 +22458,8 @@ function classDirective(name, selector) { The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure - to view the step by step details of {@link ng.$animate#addClass $animate.addClass} and - {@link ng.$animate#removeClass $animate.removeClass}. + to view the step by step details of {@link $animate#addClass $animate.addClass} and + {@link $animate#removeClass $animate.removeClass}. */ var ngClassDirective = classDirective('', true); @@ -22322,17 +22592,13 @@ var ngClassEvenDirective = classDirective('Even', 1); * document; alternatively, the css rule above must be included in the external stylesheet of the * application. * - * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they - * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css - * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below. - * * @element ANY * * @example
          {{ 'hello' }}
          -
          {{ 'hello IE7' }}
          +
          {{ 'world' }}
          it('should remove the template directive and css class', function() { @@ -22416,20 +22682,20 @@ var ngCloakDirective = ngDirective({ * * *
          - * Name: - * [ greet ]
          + * + *
          * Contact: *
            *
          • - * * * * - * - * [ clear - * | X ] + * + * + * *
          • - *
          • [ add ]
          • + *
          • *
          *
          *
          @@ -22479,12 +22745,12 @@ var ngCloakDirective = ngDirective({ * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe('john.smith@example.org'); * - * firstRepeat.element(by.linkText('clear')).click(); + * firstRepeat.element(by.buttonText('clear')).click(); * * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe(''); * - * container.element(by.linkText('add')).click(); + * container.element(by.buttonText('add')).click(); * * expect(container.element(by.repeater('contact in settings.contacts').row(2)) * .element(by.model('contact.value')) @@ -22499,20 +22765,20 @@ var ngCloakDirective = ngDirective({ * * *
          - * Name: - * [ greet ]
          + * + *
          * Contact: *
            *
          • - * * * * - * - * [ clear - * | X ] + * + * + * *
          • - *
          • [ add ]
          • + *
          • [ ]
          • *
          *
          *
          @@ -22562,12 +22828,12 @@ var ngCloakDirective = ngDirective({ * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe('john.smith@example.org'); * - * firstRepeat.element(by.linkText('clear')).click(); + * firstRepeat.element(by.buttonText('clear')).click(); * * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe(''); * - * container.element(by.linkText('add')).click(); + * container.element(by.buttonText('add')).click(); * * expect(container.element(by.repeater('contact in contacts').row(2)) * .element(by.model('contact.value')) @@ -23290,7 +23556,7 @@ forEach( * @example - Click me:
          +
          Show when checked: This is removed when the checkbox is unchecked. @@ -23417,7 +23683,7 @@ var ngIfDirective = ['$animate', function($animate) { - url of the template: {{template.url}} + url of the template: {{template.url}}
          @@ -23541,7 +23807,7 @@ var ngIfDirective = ['$animate', function($animate) { * @name ngInclude#$includeContentError * @eventType emit on the scope ngInclude was declared in * @description - * Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299) + * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299) * * @param {Object} angularEvent Synthetic event object. * @param {String} src URL of content to load. @@ -23671,7 +23937,7 @@ var ngIncludeFillContentDirective = ['$compile', * The `ngInit` directive allows you to evaluate an expression in the * current scope. * - *
          + *
          * The only appropriate use of `ngInit` is for aliasing special properties of * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you * should use {@link guide/controller controllers} rather than `ngInit` @@ -23681,7 +23947,7 @@ var ngIncludeFillContentDirective = ['$compile', * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make * sure you have parenthesis for correct precedence: *
          - *   
          + * `
          ` *
          *
          * @@ -23729,6 +23995,1487 @@ var ngInitDirective = ngDirective({ } }); +/** + * @ngdoc directive + * @name ngList + * + * @description + * Text input that converts between a delimited string and an array of strings. The default + * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom + * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. + * + * The behaviour of the directive is affected by the use of the `ngTrim` attribute. + * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each + * list item is respected. This implies that the user of the directive is responsible for + * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a + * tab or newline character. + * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected + * when joining the list items back together) and whitespace around each list item is stripped + * before it is added to the model. + * + * ### Example with Validation + * + * + * + * angular.module('listExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.names = ['morpheus', 'neo', 'trinity']; + * }]); + * + * + *
          + * + * + * + * Required! + * + *
          + * names = {{names}}
          + * myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
          + * myForm.namesInput.$error = {{myForm.namesInput.$error}}
          + * myForm.$valid = {{myForm.$valid}}
          + * myForm.$error.required = {{!!myForm.$error.required}}
          + *
          + *
          + * + * var listInput = element(by.model('names')); + * var names = element(by.exactBinding('names')); + * var valid = element(by.binding('myForm.namesInput.$valid')); + * var error = element(by.css('span.error')); + * + * it('should initialize to model', function() { + * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); + * expect(valid.getText()).toContain('true'); + * expect(error.getCssValue('display')).toBe('none'); + * }); + * + * it('should be invalid if empty', function() { + * listInput.clear(); + * listInput.sendKeys(''); + * + * expect(names.getText()).toContain(''); + * expect(valid.getText()).toContain('false'); + * expect(error.getCssValue('display')).not.toBe('none'); + * }); + * + *
          + * + * ### Example - splitting on whitespace + * + * + * + *
          {{ list | json }}
          + *
          + * + * it("should split the text by newlines", function() { + * var listInput = element(by.model('list')); + * var output = element(by.binding('list | json')); + * listInput.sendKeys('abc\ndef\nghi'); + * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); + * }); + * + *
          + * + * @element input + * @param {string=} ngList optional delimiter that should be used to split the value. + */ +var ngListDirective = function() { + return { + restrict: 'A', + priority: 100, + require: 'ngModel', + link: function(scope, element, attr, ctrl) { + // We want to control whitespace trimming so we use this convoluted approach + // to access the ngList attribute, which doesn't pre-trim the attribute + var ngList = element.attr(attr.$attr.ngList) || ', '; + var trimValues = attr.ngTrim !== 'false'; + var separator = trimValues ? trim(ngList) : ngList; + + var parse = function(viewValue) { + // If the viewValue is invalid (say required but empty) it will be `undefined` + if (isUndefined(viewValue)) return; + + var list = []; + + if (viewValue) { + forEach(viewValue.split(separator), function(value) { + if (value) list.push(trimValues ? trim(value) : value); + }); + } + + return list; + }; + + ctrl.$parsers.push(parse); + ctrl.$formatters.push(function(value) { + if (isArray(value)) { + return value.join(ngList); + } + + return undefined; + }); + + // Override the standard $isEmpty because an empty array means the input is empty. + ctrl.$isEmpty = function(value) { + return !value || !value.length; + }; + } + }; +}; + +/* global VALID_CLASS: true, + INVALID_CLASS: true, + PRISTINE_CLASS: true, + DIRTY_CLASS: true, + UNTOUCHED_CLASS: true, + TOUCHED_CLASS: true, +*/ + +var VALID_CLASS = 'ng-valid', + INVALID_CLASS = 'ng-invalid', + PRISTINE_CLASS = 'ng-pristine', + DIRTY_CLASS = 'ng-dirty', + UNTOUCHED_CLASS = 'ng-untouched', + TOUCHED_CLASS = 'ng-touched', + PENDING_CLASS = 'ng-pending'; + + +var $ngModelMinErr = new minErr('ngModel'); + +/** + * @ngdoc type + * @name ngModel.NgModelController + * + * @property {string} $viewValue Actual string value in the view. + * @property {*} $modelValue The value in the model that the control is bound to. + * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever + the control reads value from the DOM. The functions are called in array order, each passing + its return value through to the next. The last return value is forwarded to the + {@link ngModel.NgModelController#$validators `$validators`} collection. + +Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue +`$viewValue`}. + +Returning `undefined` from a parser means a parse error occurred. In that case, +no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` +will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} +is set to `true`. The parse error is stored in `ngModel.$error.parse`. + + * + * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever + the model value changes. The functions are called in reverse array order, each passing the value through to the + next. The last return value is used as the actual DOM value. + Used to format / convert values for display in the control. + * ```js + * function formatter(value) { + * if (value) { + * return value.toUpperCase(); + * } + * } + * ngModel.$formatters.push(formatter); + * ``` + * + * @property {Object.} $validators A collection of validators that are applied + * whenever the model value changes. The key value within the object refers to the name of the + * validator while the function refers to the validation operation. The validation operation is + * provided with the model value as an argument and must return a true or false value depending + * on the response of that validation. + * + * ```js + * ngModel.$validators.validCharacters = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * return /[0-9]+/.test(value) && + * /[a-z]+/.test(value) && + * /[A-Z]+/.test(value) && + * /\W+/.test(value); + * }; + * ``` + * + * @property {Object.} $asyncValidators A collection of validations that are expected to + * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided + * is expected to return a promise when it is run during the model validation process. Once the promise + * is delivered then the validation status will be set to true when fulfilled and false when rejected. + * When the asynchronous validators are triggered, each of the validators will run in parallel and the model + * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator + * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators + * will only run once all synchronous validators have passed. + * + * Please note that if $http is used then it is important that the server returns a success HTTP response code + * in order to fulfill the validation and a status level of `4xx` in order to reject the validation. + * + * ```js + * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * + * // Lookup user by username + * return $http.get('/api/users/' + value). + * then(function resolved() { + * //username exists, this means validation fails + * return $q.reject('exists'); + * }, function rejected() { + * //username does not exist, therefore this validation passes + * return true; + * }); + * }; + * ``` + * + * @property {Array.} $viewChangeListeners Array of functions to execute whenever the + * view value has changed. It is called with no arguments, and its return value is ignored. + * This can be used in place of additional $watches against the model value. + * + * @property {Object} $error An object hash with all failing validator ids as keys. + * @property {Object} $pending An object hash with all pending validator ids as keys. + * + * @property {boolean} $untouched True if control has not lost focus yet. + * @property {boolean} $touched True if control has lost focus. + * @property {boolean} $pristine True if user has not interacted with the control yet. + * @property {boolean} $dirty True if user has already interacted with the control. + * @property {boolean} $valid True if there is no error. + * @property {boolean} $invalid True if at least one error on the control. + * @property {string} $name The name attribute of the control. + * + * @description + * + * `NgModelController` provides API for the {@link ngModel `ngModel`} directive. + * The controller contains services for data-binding, validation, CSS updates, and value formatting + * and parsing. It purposefully does not contain any logic which deals with DOM rendering or + * listening to DOM events. + * Such DOM related logic should be provided by other directives which make use of + * `NgModelController` for data-binding to control elements. + * Angular provides this DOM logic for most {@link input `input`} elements. + * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example + * custom control example} that uses `ngModelController` to bind to `contenteditable` elements. + * + * @example + * ### Custom Control Example + * This example shows how to use `NgModelController` with a custom control to achieve + * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) + * collaborate together to achieve the desired result. + * + * `contenteditable` is an HTML5 attribute, which tells the browser to let the element + * contents be edited in place by the user. + * + * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} + * module to automatically remove "bad" content like inline event listener (e.g. ``). + * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks + * that content using the `$sce` service. + * + * + + [contenteditable] { + border: 1px solid black; + background-color: white; + min-height: 20px; + } + + .ng-invalid { + border: 1px solid red; + } + + + + angular.module('customControl', ['ngSanitize']). + directive('contenteditable', ['$sce', function($sce) { + return { + restrict: 'A', // only activate on element attribute + require: '?ngModel', // get a hold of NgModelController + link: function(scope, element, attrs, ngModel) { + if (!ngModel) return; // do nothing if no ng-model + + // Specify how UI should be updated + ngModel.$render = function() { + element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); + }; + + // Listen for change events to enable binding + element.on('blur keyup change', function() { + scope.$evalAsync(read); + }); + read(); // initialize + + // Write data to the model + function read() { + var html = element.html(); + // When we clear the content editable the browser leaves a
          behind + // If strip-br attribute is provided then we strip this out + if ( attrs.stripBr && html == '
          ' ) { + html = ''; + } + ngModel.$setViewValue(html); + } + } + }; + }]); +
          + +
          +
          Change me!
          + Required! +
          + +
          +
          + + it('should data-bind and become invalid', function() { + if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { + // SafariDriver can't handle contenteditable + // and Firefox driver can't clear contenteditables very well + return; + } + var contentEditable = element(by.css('[contenteditable]')); + var content = 'Change me!'; + + expect(contentEditable.getText()).toEqual(content); + + contentEditable.clear(); + contentEditable.sendKeys(protractor.Key.BACK_SPACE); + expect(contentEditable.getText()).toEqual(''); + expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); + }); + + *
          + * + * + */ +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', + function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { + this.$viewValue = Number.NaN; + this.$modelValue = Number.NaN; + this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. + this.$validators = {}; + this.$asyncValidators = {}; + this.$parsers = []; + this.$formatters = []; + this.$viewChangeListeners = []; + this.$untouched = true; + this.$touched = false; + this.$pristine = true; + this.$dirty = false; + this.$valid = true; + this.$invalid = false; + this.$error = {}; // keep invalid keys here + this.$$success = {}; // keep valid keys here + this.$pending = undefined; // keep pending keys here + this.$name = $interpolate($attr.name || '', false)($scope); + + + var parsedNgModel = $parse($attr.ngModel), + parsedNgModelAssign = parsedNgModel.assign, + ngModelGet = parsedNgModel, + ngModelSet = parsedNgModelAssign, + pendingDebounce = null, + parserValid, + ctrl = this; + + this.$$setOptions = function(options) { + ctrl.$options = options; + if (options && options.getterSetter) { + var invokeModelGetter = $parse($attr.ngModel + '()'), + invokeModelSetter = $parse($attr.ngModel + '($$$p)'); + + ngModelGet = function($scope) { + var modelValue = parsedNgModel($scope); + if (isFunction(modelValue)) { + modelValue = invokeModelGetter($scope); + } + return modelValue; + }; + ngModelSet = function($scope, newValue) { + if (isFunction(parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); + } else { + parsedNgModelAssign($scope, ctrl.$modelValue); + } + }; + } else if (!parsedNgModel.assign) { + throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", + $attr.ngModel, startingTag($element)); + } + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$render + * + * @description + * Called when the view needs to be updated. It is expected that the user of the ng-model + * directive will implement this method. + * + * The `$render()` method is invoked in the following situations: + * + * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last + * committed value then `$render()` is called to update the input control. + * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and + * 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` + * 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. + */ + this.$render = noop; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$isEmpty + * + * @description + * This is called when we need to determine if the value of an input is empty. + * + * For instance, the required directive does this to work out if the input has data or not. + * + * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. + * + * You can override this for input directives whose concept of being empty is different from the + * default. The `checkboxInputType` directive does this because in its case a value of `false` + * implies empty. + * + * @param {*} value The value of the input to check for emptiness. + * @returns {boolean} True if `value` is "empty". + */ + this.$isEmpty = function(value) { + return isUndefined(value) || value === '' || value === null || value !== value; + }; + + var parentForm = $element.inheritedData('$formController') || nullFormCtrl, + currentValidationRunId = 0; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notify the form. + * + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. + * + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * class and can be bound to as `{{someForm.someControl.$error.myError}}` . + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by Angular when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. + */ + addSetValidityMethod({ + ctrl: this, + $element: $element, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + }, + parentForm: parentForm, + $animate: $animate + }); + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setPristine + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the `ng-dirty` class and set the control to its pristine + * state (`ng-pristine` class). A model is considered to be pristine when the control + * has not been changed from when first compiled. + */ + this.$setPristine = function() { + ctrl.$dirty = false; + ctrl.$pristine = true; + $animate.removeClass($element, DIRTY_CLASS); + $animate.addClass($element, PRISTINE_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setDirty + * + * @description + * Sets the control to its dirty state. + * + * This method can be called to remove the `ng-pristine` class and set the control to its dirty + * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed + * from when first compiled. + */ + this.$setDirty = function() { + ctrl.$dirty = true; + ctrl.$pristine = false; + $animate.removeClass($element, PRISTINE_CLASS); + $animate.addClass($element, DIRTY_CLASS); + parentForm.$setDirty(); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setUntouched + * + * @description + * Sets the control to its untouched state. + * + * This method can be called to remove the `ng-touched` class and set the control to its + * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched + * by default, however this function can be used to restore that state if the model has + * already been touched by the user. + */ + this.$setUntouched = function() { + ctrl.$touched = false; + ctrl.$untouched = true; + $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setTouched + * + * @description + * Sets the control to its touched state. + * + * This method can be called to remove the `ng-untouched` class and set the control to its + * touched state (`ng-touched` class). A model is considered to be touched when the user has + * first focused the control element and then shifted focus away from the control (blur event). + */ + this.$setTouched = function() { + ctrl.$touched = true; + ctrl.$untouched = false; + $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$rollbackViewValue + * + * @description + * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, + * which may be caused by a pending debounced event or because the input is waiting for a some + * future event. + * + * If you have an input that uses `ng-model-options` to set up debounced events or events such + * as blur you can have a situation where there is a period when the `$viewValue` + * is out of synch with the ngModel's `$modelValue`. + * + * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue` + * programmatically before these debounced/future events have resolved/occurred, because Angular's + * dirty checking mechanism is not able to tell whether the model has actually changed or not. + * + * The `$rollbackViewValue()` method should be called before programmatically changing the model of an + * input which may have such events pending. This is important in order to make sure that the + * input field will be updated with the new model value and any pending operations are cancelled. + * + * + * + * angular.module('cancel-update-example', []) + * + * .controller('CancelUpdateController', ['$scope', function($scope) { + * $scope.resetWithCancel = function(e) { + * if (e.keyCode == 27) { + * $scope.myForm.myInput1.$rollbackViewValue(); + * $scope.myValue = ''; + * } + * }; + * $scope.resetWithoutCancel = function(e) { + * if (e.keyCode == 27) { + * $scope.myValue = ''; + * } + * }; + * }]); + * + * + *
          + *

          Try typing something in each input. See that the model only updates when you + * blur off the input. + *

          + *

          Now see what happens if you start typing then press the Escape key

          + * + *
          + *

          With $rollbackViewValue()

          + *
          + * myValue: "{{ myValue }}" + * + *

          Without $rollbackViewValue()

          + *
          + * myValue: "{{ myValue }}" + *
          + *
          + *
          + *
          + */ + this.$rollbackViewValue = function() { + $timeout.cancel(pendingDebounce); + ctrl.$viewValue = ctrl.$$lastCommittedViewValue; + ctrl.$render(); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$validate + * + * @description + * Runs each of the registered validators (first synchronous validators and then + * asynchronous validators). + * If the validity changes to invalid, the model will be set to `undefined`, + * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`. + * If the validity changes to valid, it will set the model to the last available valid + * `$modelValue`, i.e. either the last parsed value or the last value set from the scope. + */ + this.$validate = function() { + // ignore $validate before model is initialized + if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + return; + } + + var viewValue = ctrl.$$lastCommittedViewValue; + // Note: we use the $$rawModelValue as $modelValue might have been + // set to undefined during a view -> model update that found validation + // errors. We can't parse the view here, since that could change + // the model although neither viewValue nor the model on the scope changed + var modelValue = ctrl.$$rawModelValue; + + var prevValid = ctrl.$valid; + var prevModelValue = ctrl.$modelValue; + + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + + ctrl.$$runValidators(modelValue, viewValue, function(allValid) { + // If there was no change in validity, don't update the model + // This prevents changing an invalid modelValue to undefined + if (!allowInvalid && prevValid !== allValid) { + // Note: Don't check ctrl.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + ctrl.$modelValue = allValid ? modelValue : undefined; + + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } + } + }); + + }; + + this.$$runValidators = function(modelValue, viewValue, doneCallback) { + currentValidationRunId++; + var localValidationRunId = currentValidationRunId; + + // check parser error + if (!processParseErrors()) { + validationDone(false); + return; + } + if (!processSyncValidators()) { + validationDone(false); + return; + } + processAsyncValidators(); + + function processParseErrors() { + var errorKey = ctrl.$$parserName || 'parse'; + if (parserValid === undefined) { + setValidity(errorKey, null); + } else { + if (!parserValid) { + forEach(ctrl.$validators, function(v, name) { + setValidity(name, null); + }); + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + } + // Set the parse error last, to prevent unsetting it, should a $validators key == parserName + setValidity(errorKey, parserValid); + return parserValid; + } + return true; + } + + function processSyncValidators() { + var syncValidatorsValid = true; + forEach(ctrl.$validators, function(validator, name) { + var result = validator(modelValue, viewValue); + syncValidatorsValid = syncValidatorsValid && result; + setValidity(name, result); + }); + if (!syncValidatorsValid) { + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + return false; + } + return true; + } + + function processAsyncValidators() { + var validatorPromises = []; + var allValid = true; + forEach(ctrl.$asyncValidators, function(validator, name) { + var promise = validator(modelValue, viewValue); + if (!isPromiseLike(promise)) { + throw $ngModelMinErr("$asyncValidators", + "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); + } + setValidity(name, undefined); + validatorPromises.push(promise.then(function() { + setValidity(name, true); + }, function(error) { + allValid = false; + setValidity(name, false); + })); + }); + if (!validatorPromises.length) { + validationDone(true); + } else { + $q.all(validatorPromises).then(function() { + validationDone(allValid); + }, noop); + } + } + + function setValidity(name, isValid) { + if (localValidationRunId === currentValidationRunId) { + ctrl.$setValidity(name, isValid); + } + } + + function validationDone(allValid) { + if (localValidationRunId === currentValidationRunId) { + + doneCallback(allValid); + } + } + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$commitViewValue + * + * @description + * Commit a pending update to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + this.$commitViewValue = function() { + var viewValue = ctrl.$viewValue; + + $timeout.cancel(pendingDebounce); + + // If the view value has not changed then we should just exit, except in the case where there is + // a native validator on the element. In this case the validation state may have changed even though + // the viewValue has stayed empty. + if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { + return; + } + ctrl.$$lastCommittedViewValue = viewValue; + + // change to dirty + if (ctrl.$pristine) { + this.$setDirty(); + } + this.$$parseAndValidate(); + }; + + this.$$parseAndValidate = function() { + var viewValue = ctrl.$$lastCommittedViewValue; + var modelValue = viewValue; + parserValid = isUndefined(modelValue) ? undefined : true; + + if (parserValid) { + for (var i = 0; i < ctrl.$parsers.length; i++) { + modelValue = ctrl.$parsers[i](modelValue); + if (isUndefined(modelValue)) { + parserValid = false; + break; + } + } + } + if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + // ctrl.$modelValue has not been touched yet... + ctrl.$modelValue = ngModelGet($scope); + } + var prevModelValue = ctrl.$modelValue; + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + ctrl.$$rawModelValue = modelValue; + + if (allowInvalid) { + ctrl.$modelValue = modelValue; + writeToModelIfNeeded(); + } + + // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. + // This can happen if e.g. $setViewValue is called from inside a parser + ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + if (!allowInvalid) { + // Note: Don't check ctrl.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + ctrl.$modelValue = allValid ? modelValue : undefined; + writeToModelIfNeeded(); + } + }); + + function writeToModelIfNeeded() { + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } + } + }; + + this.$$writeModelToScope = function() { + ngModelSet($scope, ctrl.$modelValue); + forEach(ctrl.$viewChangeListeners, function(listener) { + try { + listener(); + } catch (e) { + $exceptionHandler(e); + } + }); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setViewValue + * + * @description + * Update the view value. + * + * This method should be called when an input directive want to change the view value; typically, + * this is done from within a DOM event handler. + * + * For example {@link ng.directive:input input} calls it when the value of the input changes and + * {@link ng.directive:select select} calls it when an option is selected. + * + * If the new `value` is an object (rather than a string or a number), we should make a copy of the + * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep + * watch of objects, it only looks for a change of identity. If you only change the property of + * the object then ngModel will not realise that the object has changed and will not invoke the + * `$parsers` and `$validators` pipelines. + * + * For this reason, you should not change properties of the copy once it has been passed to + * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly. + * + * When this method is called, the new `value` will be staged for committing through the `$parsers` + * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged + * value sent directly for processing, finally to be applied to `$modelValue` and then the + * **expression** specified in the `ng-model` attribute. + * + * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. + * + * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` + * and the `default` trigger is not listed, all those actions will remain pending until one of the + * `updateOn` events is triggered on the DOM element. + * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions} + * directive is used with a custom debounce for this particular event. + * + * Note that calling this function does not trigger a `$digest`. + * + * @param {string} value Value from the view. + * @param {string} trigger Event that triggered the update. + */ + this.$setViewValue = function(value, trigger) { + ctrl.$viewValue = value; + if (!ctrl.$options || ctrl.$options.updateOnDefault) { + ctrl.$$debounceViewValueCommit(trigger); + } + }; + + this.$$debounceViewValueCommit = function(trigger) { + var debounceDelay = 0, + options = ctrl.$options, + debounce; + + if (options && isDefined(options.debounce)) { + debounce = options.debounce; + if (isNumber(debounce)) { + debounceDelay = debounce; + } else if (isNumber(debounce[trigger])) { + debounceDelay = debounce[trigger]; + } else if (isNumber(debounce['default'])) { + debounceDelay = debounce['default']; + } + } + + $timeout.cancel(pendingDebounce); + if (debounceDelay) { + pendingDebounce = $timeout(function() { + ctrl.$commitViewValue(); + }, debounceDelay); + } else if ($rootScope.$$phase) { + ctrl.$commitViewValue(); + } else { + $scope.$apply(function() { + ctrl.$commitViewValue(); + }); + } + }; + + // model -> value + // Note: we cannot use a normal scope.$watch as we want to detect the following: + // 1. scope value is 'a' + // 2. user enters 'b' + // 3. ng-change kicks in and reverts scope value to 'a' + // -> scope value did not change since the last digest as + // ng-change executes in apply phase + // 4. view should be changed back to 'a' + $scope.$watch(function ngModelWatch() { + var modelValue = ngModelGet($scope); + + // if scope model value and ngModel value are out of sync + // TODO(perf): why not move this to the action fn? + if (modelValue !== ctrl.$modelValue && + // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator + (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) + ) { + ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; + parserValid = undefined; + + var formatters = ctrl.$formatters, + idx = formatters.length; + + var viewValue = modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); + } + if (ctrl.$viewValue !== viewValue) { + ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; + ctrl.$render(); + + ctrl.$$runValidators(modelValue, viewValue, noop); + } + } + + return modelValue; + }); +}]; + + +/** + * @ngdoc directive + * @name ngModel + * + * @element input + * @priority 1 + * + * @description + * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a + * property on the scope using {@link ngModel.NgModelController NgModelController}, + * which is created and exposed by this directive. + * + * `ngModel` is responsible for: + * + * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` + * require. + * - Providing validation behavior (i.e. required, number, email, url). + * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations. + * - Registering the control with its parent {@link ng.directive:form form}. + * + * Note: `ngModel` will try to bind to the property given by evaluating the expression on the + * current scope. If the property doesn't already exist on this scope, it will be created + * implicitly and added to the scope. + * + * For best practices on using `ngModel`, see: + * + * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes) + * + * For basic examples, how to use `ngModel`, see: + * + * - {@link ng.directive:input input} + * - {@link input[text] text} + * - {@link input[checkbox] checkbox} + * - {@link input[radio] radio} + * - {@link input[number] number} + * - {@link input[email] email} + * - {@link input[url] url} + * - {@link input[date] date} + * - {@link input[datetime-local] datetime-local} + * - {@link input[time] time} + * - {@link input[month] month} + * - {@link input[week] week} + * - {@link ng.directive:select select} + * - {@link ng.directive:textarea textarea} + * + * # CSS classes + * The following CSS classes are added and removed on the associated input/select/textarea element + * depending on the validity of the model. + * + * - `ng-valid`: the model is valid + * - `ng-invalid`: the model is invalid + * - `ng-valid-[key]`: for each valid key added by `$setValidity` + * - `ng-invalid-[key]`: for each invalid key added by `$setValidity` + * - `ng-pristine`: the control hasn't been interacted with yet + * - `ng-dirty`: the control has been interacted with + * - `ng-touched`: the control has been blurred + * - `ng-untouched`: the control hasn't been blurred + * - `ng-pending`: any `$asyncValidators` are unfulfilled + * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * + * ## Animation Hooks + * + * Animations within models are triggered when any of the associated CSS classes are added and removed + * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, + * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. + * The animations that are triggered within ngModel are similar to how they work in ngClass and + * animations can be hooked into using CSS transitions, keyframes as well as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style an input element + * that has been rendered as invalid after it has been validated: + * + *
          + * //be sure to include ngAnimate as a module to hook into more
          + * //advanced animations
          + * .my-input {
          + *   transition:0.5s linear all;
          + *   background: white;
          + * }
          + * .my-input.ng-invalid {
          + *   background: red;
          + *   color:white;
          + * }
          + * 
          + * + * @example + * + + + +

          + Update input to see transitions when valid/invalid. + Integer is a valid value. +

          +
          + +
          +
          + *
          + * + * ## Binding to a getter/setter + * + * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a + * function that returns a representation of the model when called with zero arguments, and sets + * the internal state of a model when called with an argument. It's sometimes useful to use this + * for models that have an internal representation that's different from what the model exposes + * to the view. + * + *
          + * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more + * frequently than other parts of your code. + *
          + * + * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that + * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to + * a `
          `, which will enable this behavior for all ``s within it. See + * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. + * + * The following example shows how to use `ngModel` with a getter/setter: + * + * @example + * + +
          + + + +
          user.name = 
          +
          +
          + + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function(newName) { + // Note that newName can be undefined for two reasons: + // 1. Because it is called as a getter and thus called with no arguments + // 2. Because the property should actually be set to undefined. This happens e.g. if the + // input is invalid + return arguments.length ? (_name = newName) : _name; + } + }; + }]); + + *
          + */ +var ngModelDirective = ['$rootScope', function($rootScope) { + return { + restrict: 'A', + require: ['ngModel', '^?form', '^?ngModelOptions'], + controller: NgModelController, + // Prelink needs to run before any input directive + // so that we can set the NgModelOptions in NgModelController + // before anyone else uses it. + priority: 1, + compile: function ngModelCompile(element) { + // Setup initial state of the control + element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS); + + return { + pre: function ngModelPreLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0], + formCtrl = ctrls[1] || nullFormCtrl; + + modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); + + // notify others, especially parent forms + formCtrl.$addControl(modelCtrl); + + attr.$observe('name', function(newValue) { + if (modelCtrl.$name !== newValue) { + formCtrl.$$renameControl(modelCtrl, newValue); + } + }); + + scope.$on('$destroy', function() { + formCtrl.$removeControl(modelCtrl); + }); + }, + post: function ngModelPostLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0]; + if (modelCtrl.$options && modelCtrl.$options.updateOn) { + element.on(modelCtrl.$options.updateOn, function(ev) { + modelCtrl.$$debounceViewValueCommit(ev && ev.type); + }); + } + + element.on('blur', function(ev) { + if (modelCtrl.$touched) return; + + if ($rootScope.$$phase) { + scope.$evalAsync(modelCtrl.$setTouched); + } else { + scope.$apply(modelCtrl.$setTouched); + } + }); + } + }; + } + }; +}]; + +var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; + +/** + * @ngdoc directive + * @name ngModelOptions + * + * @description + * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of + * events that will trigger a model update and/or a debouncing delay so that the actual update only + * takes place when a timer expires; this timer will be reset after another change takes place. + * + * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might + * be different from the value in the actual model. This means that if you update the model you + * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in + * order to make sure it is synchronized with the model and that any debounced action is canceled. + * + * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`} + * method is by making sure the input is placed inside a form that has a `name` attribute. This is + * important because `form` controllers are published to the related scope under the name in their + * `name` attribute. + * + * Any pending changes will take place immediately when an enclosing form is submitted via the + * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. + * + * `ngModelOptions` has an effect on the element it's declared on and its descendants. + * + * @param {Object} ngModelOptions options to apply to the current model. Valid keys are: + * - `updateOn`: string specifying which event should the input be bound to. You can set several + * events using an space delimited list. There is a special event called `default` that + * matches the default events belonging of the control. + * - `debounce`: integer value which contains the debounce model update value in milliseconds. A + * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a + * custom value for each event. For example: + * `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"` + * - `allowInvalid`: boolean value which indicates that the model can be set with values that did + * not validate correctly instead of the default behavior of setting the model to undefined. + * - `getterSetter`: boolean value which determines whether or not to treat functions bound to + `ngModel` as getters/setters. + * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for + * ``, ``, ... . It understands UTC/GMT and the + * continental US time zone abbreviations, but for general use, use a time zone offset, for + * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) + * If not specified, the timezone of the browser will be used. + * + * @example + + The following example shows how to override immediate updates. Changes on the inputs within the + form will update the model only when the control loses focus (blur event). If `escape` key is + pressed while the input field is focused, the value is reset to the value in the current model. + + + +
          +
          +
          +
          +
          +
          user.name = 
          +
          +
          + + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'say', data: '' }; + + $scope.cancel = function(e) { + if (e.keyCode == 27) { + $scope.userForm.userName.$rollbackViewValue(); + } + }; + }]); + + + var model = element(by.binding('user.name')); + var input = element(by.model('user.name')); + var other = element(by.model('user.data')); + + it('should allow custom events', function() { + input.sendKeys(' hello'); + input.click(); + expect(model.getText()).toEqual('say'); + other.click(); + expect(model.getText()).toEqual('say hello'); + }); + + it('should $rollbackViewValue when model changes', function() { + input.sendKeys(' hello'); + expect(input.getAttribute('value')).toEqual('say hello'); + input.sendKeys(protractor.Key.ESCAPE); + expect(input.getAttribute('value')).toEqual('say'); + other.click(); + expect(model.getText()).toEqual('say'); + }); + +
          + + This one shows how to debounce model changes. Model will be updated only 1 sec after last change. + If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. + + + +
          +
          + + +
          +
          +
          user.name = 
          +
          +
          + + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'say' }; + }]); + +
          + + This one shows how to bind to getter/setters: + + + +
          +
          + +
          +
          user.name = 
          +
          +
          + + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function(newName) { + // Note that newName can be undefined for two reasons: + // 1. Because it is called as a getter and thus called with no arguments + // 2. Because the property should actually be set to undefined. This happens e.g. if the + // input is invalid + return arguments.length ? (_name = newName) : _name; + } + }; + }]); + +
          + */ +var ngModelOptionsDirective = function() { + return { + restrict: 'A', + controller: ['$scope', '$attrs', function($scope, $attrs) { + var that = this; + this.$options = copy($scope.$eval($attrs.ngModelOptions)); + // Allow adding/overriding bound events + if (this.$options.updateOn !== undefined) { + this.$options.updateOnDefault = false; + // extract "default" pseudo-event from list of events that can trigger a model update + this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() { + that.$options.updateOnDefault = true; + return ' '; + })); + } else { + this.$options.updateOnDefault = true; + } + }] + }; +}; + + + +// helper methods +function addSetValidityMethod(context) { + var ctrl = context.ctrl, + $element = context.$element, + classCache = {}, + set = context.set, + unset = context.unset, + parentForm = context.parentForm, + $animate = context.$animate; + + classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); + + ctrl.$setValidity = setValidity; + + function setValidity(validationErrorKey, state, controller) { + if (state === undefined) { + createAndSet('$pending', validationErrorKey, controller); + } else { + unsetAndCleanup('$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(ctrl.$error, validationErrorKey, controller); + unset(ctrl.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(ctrl.$error, validationErrorKey, controller); + set(ctrl.$$success, validationErrorKey, controller); + } else { + set(ctrl.$error, validationErrorKey, controller); + unset(ctrl.$$success, validationErrorKey, controller); + } + } + if (ctrl.$pending) { + cachedToggleClass(PENDING_CLASS, true); + ctrl.$valid = ctrl.$invalid = undefined; + toggleValidationCss('', null); + } else { + cachedToggleClass(PENDING_CLASS, false); + ctrl.$valid = isObjectEmpty(ctrl.$error); + ctrl.$invalid = !ctrl.$valid; + toggleValidationCss('', ctrl.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in ctrl.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (ctrl.$pending && ctrl.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (ctrl.$error[validationErrorKey]) { + combinedState = false; + } else if (ctrl.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(validationErrorKey, combinedState); + parentForm.$setValidity(validationErrorKey, combinedState, ctrl); + } + + function createAndSet(name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(className, switchValue) { + if (switchValue && !classCache[className]) { + $animate.addClass($element, className); + classCache[className] = true; + } else if (!switchValue && classCache[className]) { + $animate.removeClass($element, className); + classCache[className] = false; + } + } + + function toggleValidationCss(validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + return false; + } + } + return true; +} + /** * @ngdoc directive * @name ngNonBindable @@ -23763,6 +25510,725 @@ var ngInitDirective = ngDirective({ */ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); +/* global jqLiteRemove */ + +var ngOptionsMinErr = minErr('ngOptions'); + +/** + * @ngdoc directive + * @name ngOptions + * @restrict A + * + * @description + * + * The `ngOptions` attribute can be used to dynamically generate a list of `
        • +
        • + +
        • +
        +
        +
        +
        + +
        + +
        + + + + Select . +
        +
        + Currently selected: {{ {selected_color:myColor} }} +
        +
        +
      + + + it('should check ng-options', function() { + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); + element.all(by.model('myColor')).first().click(); + element.all(by.css('select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); + element(by.css('.nullable select[ng-model="myColor"]')).click(); + element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); + }); + + + */ + +// jshint maxlen: false +// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999 +var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/; + // 1: value expression (valueFn) + // 2: label expression (displayFn) + // 3: group by expression (groupByFn) + // 4: disable when expression (disableWhenFn) + // 5: array item variable name + // 6: object item key variable name + // 7: object item value variable name + // 8: collection expression + // 9: track by expression +// jshint maxlen: 100 + + +var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { + + function parseOptionsExpression(optionsExp, selectElement, scope) { + + var match = optionsExp.match(NG_OPTIONS_REGEXP); + if (!(match)) { + throw ngOptionsMinErr('iexp', + "Expected expression in form of " + + "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + + " but got '{0}'. Element: {1}", + optionsExp, startingTag(selectElement)); + } + + // Extract the parts from the ngOptions expression + + // The variable name for the value of the item in the collection + var valueName = match[5] || match[7]; + // The variable name for the key of the item in the collection + var keyName = match[6]; + + // An expression that generates the viewValue for an option if there is a label expression + var selectAs = / as /.test(match[0]) && match[1]; + // An expression that is used to track the id of each object in the options collection + var trackBy = match[9]; + // An expression that generates the viewValue for an option if there is no label expression + var valueFn = $parse(match[2] ? match[1] : valueName); + var selectAsFn = selectAs && $parse(selectAs); + var viewValueFn = selectAsFn || valueFn; + var trackByFn = trackBy && $parse(trackBy); + + // Get the value by which we are going to track the option + // if we have a trackFn then use that (passing scope and locals) + // otherwise just hash the given viewValue + var getTrackByValueFn = trackBy ? + function(value, locals) { return trackByFn(scope, locals); } : + function getHashOfValue(value) { return hashKey(value); }; + var getTrackByValue = function(value, key) { + return getTrackByValueFn(value, getLocals(value, key)); + }; + + var displayFn = $parse(match[2] || match[1]); + var groupByFn = $parse(match[3] || ''); + var disableWhenFn = $parse(match[4] || ''); + var valuesFn = $parse(match[8]); + + var locals = {}; + var getLocals = keyName ? function(value, key) { + locals[keyName] = key; + locals[valueName] = value; + return locals; + } : function(value) { + locals[valueName] = value; + return locals; + }; + + + function Option(selectValue, viewValue, label, group, disabled) { + this.selectValue = selectValue; + this.viewValue = viewValue; + this.label = label; + this.group = group; + this.disabled = disabled; + } + + return { + trackBy: trackBy, + getTrackByValue: getTrackByValue, + getWatchables: $parse(valuesFn, function(values) { + // Create a collection of things that we would like to watch (watchedArray) + // so that they can all be watched using a single $watchCollection + // that only runs the handler once if anything changes + var watchedArray = []; + values = values || []; + + Object.keys(values).forEach(function getWatchable(key) { + var locals = getLocals(values[key], key); + var selectValue = getTrackByValueFn(values[key], locals); + watchedArray.push(selectValue); + + // Only need to watch the displayFn if there is a specific label expression + if (match[2] || match[1]) { + var label = displayFn(scope, locals); + watchedArray.push(label); + } + + // Only need to watch the disableWhenFn if there is a specific disable expression + if (match[4]) { + var disableWhen = disableWhenFn(scope, locals); + watchedArray.push(disableWhen); + } + }); + return watchedArray; + }), + + getOptions: function() { + + var optionItems = []; + var selectValueMap = {}; + + // The option values were already computed in the `getWatchables` fn, + // which must have been called to trigger `getOptions` + var optionValues = valuesFn(scope) || []; + var optionValuesKeys; + + + if (!keyName && isArrayLike(optionValues)) { + optionValuesKeys = optionValues; + } else { + // if object, extract keys, in enumeration order, unsorted + optionValuesKeys = []; + for (var itemKey in optionValues) { + if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') { + optionValuesKeys.push(itemKey); + } + } + } + + var optionValuesLength = optionValuesKeys.length; + + for (var index = 0; index < optionValuesLength; index++) { + var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index]; + var value = optionValues[key]; + var locals = getLocals(value, key); + var viewValue = viewValueFn(scope, locals); + var selectValue = getTrackByValueFn(viewValue, locals); + var label = displayFn(scope, locals); + var group = groupByFn(scope, locals); + var disabled = disableWhenFn(scope, locals); + var optionItem = new Option(selectValue, viewValue, label, group, disabled); + + optionItems.push(optionItem); + selectValueMap[selectValue] = optionItem; + } + + return { + items: optionItems, + selectValueMap: selectValueMap, + getOptionFromViewValue: function(value) { + return selectValueMap[getTrackByValue(value)]; + }, + getViewValueFromOption: function(option) { + // If the viewValue could be an object that may be mutated by the application, + // we need to make a copy and not return the reference to the value on the option. + return trackBy ? angular.copy(option.viewValue) : option.viewValue; + } + }; + } + }; + } + + + // we can't just jqLite('
    - - - it('should check ng-options', function() { - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); - element.all(by.model('myColor')).first().click(); - element.all(by.css('select[ng-model="myColor"] option')).first().click(); - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); - element(by.css('.nullable select[ng-model="myColor"]')).click(); - element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); - }); - - */ - -var ngOptionsDirective = valueFn({ - restrict: 'A', - terminal: true -}); - -// jshint maxlen: false -var selectDirective = ['$compile', '$parse', function($compile, $parse) { - //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 - var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/, - nullModelCtrl = {$setViewValue: noop}; -// jshint maxlen: 100 +var selectDirective = function() { return { restrict: 'E', require: ['select', '?ngModel'], - controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { - var self = this, - optionsMap = {}, - ngModelCtrl = nullModelCtrl, - nullOption, - unknownOption; - - - self.databound = $attrs.ngModel; - - - self.init = function(ngModelCtrl_, nullOption_, unknownOption_) { - ngModelCtrl = ngModelCtrl_; - nullOption = nullOption_; - unknownOption = unknownOption_; - }; - - - self.addOption = function(value, element) { - assertNotHasOwnProperty(value, '"option value"'); - optionsMap[value] = true; - - if (ngModelCtrl.$viewValue == value) { - $element.val(value); - if (unknownOption.parent()) unknownOption.remove(); - } - // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459 - // Adding an