Merge remote-tracking branch 'grafana/master' into alpha-text2

* grafana/master: (40 commits)
  Minor refactoring of PR #15770
  Alternative fix to detecting when to stop a playlist, fixes #15701 and #15702
  fix discord notifier so it doesn't crash when there are no image generated
  fix: Consistency in unit labels #15709
  Update latest.json
  Update CHANGELOG.md
  position from add panel, dimensions from copied panel
  changelog: add notes about closing #14509 #15179
  fix: Add class for input fields with help icon to avoid icon hiding the text #15771
  Update CHANGELOG.md
  restore to current folder when restoring old dashboard version
  fix(renderer): Vendor ansicolor as typescript
  log phantomjs output even if it timeout and include orgId when render alert
  keep size from copied panel and not from add panel widget
  Added basic cherry pick helping task
  Prevent search in VizPicker from stealing focus (#15802)
  fix only users that can edit a dashboard should be able to update panel json
  Updated changelog task
  Fixed image rendering issue for dashboards with auto refresh, casued by missing reloadOnSearch flag on route, fixes #15631
  Updated to add PR author, skip PR issue references
  ...
This commit is contained in:
ryan 2019-03-06 15:47:14 -08:00
commit 627a32e9bb
163 changed files with 4898 additions and 4810 deletions

View File

@ -6,25 +6,34 @@
### Minor
* **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
### Bug Fixes
* **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
* **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239)
* **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739)
* **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
# 6.0.1 (unreleased)
# 6.0.1 (2019-03-06)
### Bug Fixes
* **Metrics**: Fixes broken usagestats metrics for /metrics [#15651](https://github.com/grafana/grafana/issues/15651)
* **Dashboard**: Fixes kiosk mode should have &kiosk appended to the url [#15765](https://github.com/grafana/grafana/issues/15765)
* **Dashboard**: Fixes kiosk=tv mode with autofitpanels should respect header [#15650](https://github.com/grafana/grafana/issues/15650)
* **Image rendering**: Fixed image rendering issue for dashboards with auto refresh, . [#15818](https://github.com/grafana/grafana/pull/15818), [@torkelo](https://github.com/torkelo)
* **Dashboard**: Fix only users that can edit a dashboard should be able to update panel json. [#15805](https://github.com/grafana/grafana/pull/15805), [@marefr](https://github.com/marefr)
* **LDAP**: fix allow anonymous initial bind for ldap search. [#15803](https://github.com/grafana/grafana/pull/15803), [@marefr](https://github.com/marefr)
* **UX**: Fixed scrollbar not visible initially (only after manual scroll). [#15798](https://github.com/grafana/grafana/pull/15798), [@torkelo](https://github.com/torkelo)
* **Datasource admin** TestData [#15793](https://github.com/grafana/grafana/pull/15793), [@hugohaggmark](https://github.com/hugohaggmark)
* **Dashboard**: Fixed scrolling issue that caused scroll to be locked to bottom. [#15792](https://github.com/grafana/grafana/pull/15792), [@torkelo](https://github.com/torkelo)
* **Explore**: Viewers with viewers_can_edit should be able to access /explore. [#15787](https://github.com/grafana/grafana/pull/15787), [@jschill](https://github.com/jschill)
* **Security** fix: limit access to org admin and alerting pages. [#15761](https://github.com/grafana/grafana/pull/15761), [@marefr](https://github.com/marefr)
* **Panel Edit** minInterval changes did not persist [#15757](https://github.com/grafana/grafana/pull/15757), [@hugohaggmark](https://github.com/hugohaggmark)
* **Teams**: Fixed bug when getting teams for user. [#15595](https://github.com/grafana/grafana/pull/15595), [@hugohaggmark](https://github.com/hugohaggmark)
* **Stackdriver**: fix for float64 bounds for distribution metrics [#14509](https://github.com/grafana/grafana/issues/14509)
* **Stackdriver**: no reducers available for distribution type [#15179](https://github.com/grafana/grafana/issues/15179)
# 6.0.0 stable (2019-02-25)
### Bug Fixes
* **Stackdriver**: fix for float64 bounds for distribution metrics [#14509](https://github.com/grafana/grafana/issues/14509)
* **Stackdriver**: no reducers available for distribution type [#15179](https://github.com/grafana/grafana/issues/15179)
* **Dashboard**: fixes click after scroll in series override menu [#15621](https://github.com/grafana/grafana/issues/15621)
* **MySQL**: fix mysql query using _interval_ms variable throws error [#14507](https://github.com/grafana/grafana/issues/14507)

312
Gopkg.lock generated
View File

@ -2,30 +2,39 @@
[[projects]]
digest = "1:f8ad8a53fa865a70efbe215b0ca34735523f50ea39e0efde319ab6fc80089b44"
name = "cloud.google.com/go"
packages = ["compute/metadata"]
pruneopts = "NUT"
revision = "056a55f54a6cc77b440b31a56a5e7c3982d32811"
version = "v0.22.0"
[[projects]]
digest = "1:167b6f65a6656de568092189ae791253939f076df60231fdd64588ac703892a1"
name = "github.com/BurntSushi/toml"
packages = ["."]
pruneopts = "NUT"
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
version = "v0.3.0"
[[projects]]
branch = "master"
digest = "1:7d23e6e1889b8bb4bbb37a564708fdab4497ce232c3a99d66406c975b642a6ff"
name = "github.com/Unknwon/com"
packages = ["."]
pruneopts = "NUT"
revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520"
[[projects]]
branch = "master"
digest = "1:1610787cd9726e29d8fecc2a80e43e4fced008a1f560fec6688fc4d946f17835"
name = "github.com/VividCortex/mysqlerr"
packages = ["."]
pruneopts = "NUT"
revision = "6c6b55f8796f578c870b7e19bafb16103bc40095"
[[projects]]
digest = "1:ebe102b61c1615d2954734e3cfe1b6b06a5088c25a41055b38661d41ad7b8f27"
name = "github.com/aws/aws-sdk-go"
packages = [
"aws",
@ -69,399 +78,507 @@
"service/resourcegroupstaggingapi",
"service/resourcegroupstaggingapi/resourcegroupstaggingapiiface",
"service/s3",
"service/sts"
"service/sts",
]
pruneopts = "NUT"
revision = "62936e15518acb527a1a9cb4a39d96d94d0fd9a2"
version = "v1.16.15"
[[projects]]
branch = "master"
digest = "1:79cad073c7be02632d3fa52f62486848b089f560db1e94536de83a408c0f4726"
name = "github.com/benbjohnson/clock"
packages = ["."]
pruneopts = "NUT"
revision = "7dc76406b6d3c05b5f71a86293cbcf3c4ea03b19"
[[projects]]
branch = "master"
digest = "1:707ebe952a8b3d00b343c01536c79c73771d100f63ec6babeaed5c79e2b8a8dd"
name = "github.com/beorn7/perks"
packages = ["quantile"]
pruneopts = "NUT"
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
branch = "master"
digest = "1:433a2ff0ef4e2f8634614aab3174783c5ff80120b487712db96cc3712f409583"
name = "github.com/bmizerany/assert"
packages = ["."]
pruneopts = "NUT"
revision = "b7ed37b82869576c289d7d97fb2bbd8b64a0cb28"
[[projects]]
branch = "master"
digest = "1:d8f9145c361920507a4f85ffb7f70b96beaedacba2ce8c00aa663adb08689d3e"
name = "github.com/bradfitz/gomemcache"
packages = ["memcache"]
pruneopts = "NUT"
revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b"
[[projects]]
branch = "master"
digest = "1:8ecb89af7dfe3ac401bdb0c9390b134ef96a97e85f732d2b0604fb7b3977839f"
name = "github.com/codahale/hdrhistogram"
packages = ["."]
pruneopts = "NUT"
revision = "3a0bb77429bd3a61596f5e8a3172445844342120"
[[projects]]
digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58"
name = "github.com/codegangsta/cli"
packages = ["."]
pruneopts = "NUT"
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
version = "v1.20.0"
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "NUT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
digest = "1:1b318d2dd6cea8a1a8d8ec70348852303bd3e491df74e8bca6e32eb5a4d06970"
name = "github.com/denisenkom/go-mssqldb"
packages = [
".",
"internal/cp"
"internal/cp",
]
pruneopts = "NUT"
revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
[[projects]]
branch = "master"
digest = "1:2da5f11ad66ff01a27a5c3dba4620b7eee2327be75b32c9ee9f87c9a8001ecbf"
name = "github.com/facebookgo/inject"
packages = ["."]
pruneopts = "NUT"
revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b"
[[projects]]
branch = "master"
digest = "1:1108df7f658c90db041e0d6174d55be689aaeb0585913b9c3c7aab51a3a6b2b1"
name = "github.com/facebookgo/structtag"
packages = ["."]
pruneopts = "NUT"
revision = "217e25fb96916cc60332e399c9aa63f5c422ceed"
[[projects]]
digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45"
name = "github.com/fatih/color"
packages = ["."]
pruneopts = "NUT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
name = "github.com/go-ini/ini"
packages = ["."]
revision = "6529cf7c58879c08d927016dde4477f18a0634cb"
version = "v1.36.0"
[[projects]]
branch = "master"
digest = "1:682a0aca743a1a4a36697f3d7f86c0ed403c4e3a780db9935f633242855eac9c"
name = "github.com/go-macaron/binding"
packages = ["."]
pruneopts = "NUT"
revision = "ac54ee249c27dca7e76fad851a4a04b73bd1b183"
[[projects]]
branch = "master"
digest = "1:6326b27f8e0c8e135c8674ddbc619fae879664ac832e8e6fa6a23ce0d279ed4d"
name = "github.com/go-macaron/gzip"
packages = ["."]
pruneopts = "NUT"
revision = "cad1c6580a07c56f5f6bc52d66002a05985c5854"
[[projects]]
branch = "master"
digest = "1:fb8711b648d1ff03104fc1d9593a13cb1d5120be7ba2b01641c14ccae286a9e3"
name = "github.com/go-macaron/inject"
packages = ["."]
pruneopts = "NUT"
revision = "d8a0b8677191f4380287cfebd08e462217bac7ad"
[[projects]]
branch = "master"
digest = "1:21577aafe885f088e8086a3415f154c63c0b7ce956a6994df2ac5776bc01b7e3"
name = "github.com/go-macaron/session"
packages = [
".",
"memcache",
"postgres",
"redis"
"redis",
]
pruneopts = "NUT"
revision = "068d408f9c54c7fa7fcc5e2bdd3241ab21280c9e"
[[projects]]
digest = "1:fddd4bada6100d6fc49a9f32f18ba5718db45a58e4b00aa6377e1cfbf06af34f"
name = "github.com/go-sql-driver/mysql"
packages = ["."]
pruneopts = "NUT"
revision = "2cc627ac8defc45d65066ae98f898166f580f9a4"
[[projects]]
digest = "1:a1efdbc2762667c8a41cbf02b19a0549c846bf2c1d08cad4f445e3344089f1f0"
name = "github.com/go-stack/stack"
packages = ["."]
pruneopts = "NUT"
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
version = "v1.7.0"
[[projects]]
digest = "1:06d21295033f211588d0ad7ff391cc1b27e72b60cb6d4b7db0d70cffae4cf228"
name = "github.com/go-xorm/builder"
packages = ["."]
revision = "bad0a612f0d6277b953910822ab5dfb30dd18237"
version = "v0.2.0"
pruneopts = "NUT"
revision = "1d658d7596c25394aab557ef5b50ef35bf706384"
version = "v0.3.4"
[[projects]]
digest = "1:b26928aab0fff92592e8728c5bc9d6e404fa2017d6a8e841ae5e60a42237f6fc"
name = "github.com/go-xorm/core"
packages = ["."]
revision = "da1adaf7a28ca792961721a34e6e04945200c890"
version = "v0.5.7"
pruneopts = "NUT"
revision = "ccc80c1adf1f6172bbc548877f50a1163041a40a"
version = "v0.6.2"
[[projects]]
digest = "1:407316703b32d68ccf5d39bdae57d411b6954e253e07d0fff0988a3f39861f2f"
name = "github.com/go-xorm/xorm"
packages = ["."]
revision = "1933dd69e294c0a26c0266637067f24dbb25770c"
version = "v0.6.4"
pruneopts = "NUT"
revision = "1f39c590c64924f358c0d89016ac9b2bb84e9125"
version = "v0.7.1"
[[projects]]
branch = "master"
digest = "1:ffbb19fb66f140b5ea059428d1f84246a055d1bc3d9456c1e5c3d143611f03d0"
name = "github.com/golang/protobuf"
packages = [
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp"
"ptypes/timestamp",
]
pruneopts = "NUT"
revision = "927b65914520a8b7d44f5c9057611cfec6b2e2d0"
[[projects]]
branch = "master"
digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795"
name = "github.com/gopherjs/gopherjs"
packages = ["js"]
pruneopts = "NUT"
revision = "8dffc02ea1cb8398bb73f30424697c60fcf8d4c5"
[[projects]]
digest = "1:3b708ebf63bfa9ba3313bedb8526bc0bb284e51474e65e958481476a9d4a12aa"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "NUT"
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
digest = "1:4e771d1c6e15ca4516ad971c34205c822b5cff2747179679d7b321e4e1bfe431"
name = "github.com/gosimple/slug"
packages = ["."]
pruneopts = "NUT"
revision = "e9f42fa127660e552d0ad2b589868d403a9be7c6"
version = "v1.1.1"
[[projects]]
branch = "master"
digest = "1:08e53c69cd267ef7d71eeae5d953153d0d2bc1b8e0b498731fe9acaead7001b6"
name = "github.com/grafana/grafana-plugin-model"
packages = [
"go/datasource",
"go/renderer"
"go/renderer",
]
pruneopts = "NUT"
revision = "84176c64269d8060f99e750ee8aba6f062753336"
[[projects]]
branch = "master"
digest = "1:58ba5285227b0f635652cd4aa82c4cfd00b590191eadd823462f0c9f64e3ae07"
name = "github.com/hashicorp/go-hclog"
packages = ["."]
pruneopts = "NUT"
revision = "69ff559dc25f3b435631604f573a5fa1efdb6433"
[[projects]]
digest = "1:532090ffc3b05a7e4c0229dd2698d79149f2e0683df993224a8b202f607fb605"
name = "github.com/hashicorp/go-plugin"
packages = ["."]
pruneopts = "NUT"
revision = "e8d22c780116115ae5624720c9af0c97afe4f551"
[[projects]]
branch = "master"
digest = "1:8925116d1edcd85fc0c014e1aa69ce12892489b48ee633a605c46d893b8c151f"
name = "github.com/hashicorp/go-version"
packages = ["."]
pruneopts = "NUT"
revision = "23480c0665776210b5fbbac6eaaee40e3e6a96b7"
[[projects]]
branch = "master"
digest = "1:8deb0c5545c824dfeb0ac77ab8eb67a3d541eab76df5c85ce93064ef02d44cd0"
name = "github.com/hashicorp/yamux"
packages = ["."]
pruneopts = "NUT"
revision = "7221087c3d281fda5f794e28c2ea4c6e4d5c4558"
[[projects]]
digest = "1:efbe016b6d198cf44f1db0ed2fbdf1b36ebf1f6956cc9b76d6affa96f022d368"
name = "github.com/inconshreveable/log15"
packages = ["."]
pruneopts = "NUT"
revision = "0decfc6c20d9ca0ad143b0e89dcaa20f810b4fb3"
version = "v2.13"
[[projects]]
digest = "1:1f2aebae7e7c856562355ec0198d8ca2fa222fb05e5b1b66632a1fce39631885"
name = "github.com/jmespath/go-jmespath"
packages = ["."]
revision = "0b12d6b5"
pruneopts = "NUT"
revision = "c2b33e84"
[[projects]]
digest = "1:6ddab442e52381bab82fb6c07ef3f4b565ff7ec4b8fae96d8dd4b8573a460597"
name = "github.com/jtolds/gls"
packages = ["."]
pruneopts = "NUT"
revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064"
version = "v4.2.1"
[[projects]]
digest = "1:1da1796a71eb70f1e3e085984d044f67840bb0326816ec8276231aa87b1b9fc3"
name = "github.com/klauspost/compress"
packages = [
"flate",
"gzip"
"gzip",
]
pruneopts = "NUT"
revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf"
version = "v1.2.1"
[[projects]]
digest = "1:5e55a8699c9ff7aba1e4c8952aeda209685d88d4cb63a8766c338e333b8e65d6"
name = "github.com/klauspost/cpuid"
packages = ["."]
pruneopts = "NUT"
revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da"
version = "v1.1"
[[projects]]
digest = "1:b95da1293525625ef6f07be79d537b9bf2ecd7901efcf9a92193edafbd55b9ef"
name = "github.com/klauspost/crc32"
packages = ["."]
pruneopts = "NUT"
revision = "cb6bfca970f6908083f26f39a79009d608efd5cd"
version = "v1.1"
[[projects]]
digest = "1:7b21c7fc5551b46d1308b4ffa9e9e49b66c7a8b0ba88c0130474b0e7a20d859f"
name = "github.com/kr/pretty"
packages = ["."]
pruneopts = "NUT"
revision = "73f6ac0b30a98e433b289500d779f50c1a6f0712"
version = "v0.1.0"
[[projects]]
digest = "1:c3a7836b5904db0f8b609595b619916a6831cb35b8b714aec39f96d00c6155d8"
name = "github.com/kr/text"
packages = ["."]
pruneopts = "NUT"
revision = "e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f"
version = "v0.1.0"
[[projects]]
branch = "master"
digest = "1:7a1e592f0349d56fac8ce47f28469e4e7f4ce637cb26f40c88da9dff25db1c98"
name = "github.com/lib/pq"
packages = [
".",
"oid"
"oid",
]
pruneopts = "NUT"
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
[[projects]]
digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "NUT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "NUT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
digest = "1:536979f1c56397dbf91c2785159b37dec37e35d3bffa3cd1cfe66d25f51f8088"
name = "github.com/mattn/go-sqlite3"
packages = ["."]
pruneopts = "NUT"
revision = "323a32be5a2421b8c7087225079c6c900ec397cd"
version = "v1.7.0"
[[projects]]
digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
pruneopts = "NUT"
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048"
name = "github.com/mitchellh/go-testing-interface"
packages = ["."]
pruneopts = "NUT"
revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
[[projects]]
digest = "1:3b517122f3aad1ecce45a630ea912b3092b4729f25532a911d0cb2935a1f9352"
name = "github.com/oklog/run"
packages = ["."]
pruneopts = "NUT"
revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39"
version = "v1.0.0"
[[projects]]
digest = "1:7da29c22bcc5c2ffb308324377dc00b5084650348c2799e573ed226d8cc9faf0"
name = "github.com/opentracing/opentracing-go"
packages = [
".",
"ext",
"log"
"log",
]
pruneopts = "NUT"
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
[[projects]]
digest = "1:748946761cf99c8b73cef5a3c0ee3e040859dd713a20cece0d0e0dc04e6ceca7"
name = "github.com/patrickmn/go-cache"
packages = ["."]
pruneopts = "NUT"
revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
version = "v2.1.0"
[[projects]]
digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "NUT"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:4759bed95e3a52febc18c071db28790a5c6e9e106ee201a37add6f6a056f8f9c"
name = "github.com/prometheus/client_golang"
packages = [
"api",
"api/prometheus/v1",
"prometheus",
"prometheus/promhttp"
"prometheus/promhttp",
]
pruneopts = "NUT"
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
version = "v0.9.0-pre1"
[[projects]]
branch = "master"
digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b"
name = "github.com/prometheus/client_model"
packages = ["go"]
pruneopts = "NUT"
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
digest = "1:768b555b86742de2f28beb37f1dedce9a75f91f871d75b5717c96399c1a78c08"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
"model",
]
pruneopts = "NUT"
revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
[[projects]]
branch = "master"
digest = "1:c4a213a8d73fbb0b13f717ba7996116602ef18ecb42b91d77405877914cb0349"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
"xfs",
]
pruneopts = "NUT"
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
[[projects]]
branch = "master"
digest = "1:16e2136a67ec44aa2d1d6b0fd65394b3c4a8b2a1b6730c77967f7b7b06b179b2"
name = "github.com/rainycape/unidecode"
packages = ["."]
pruneopts = "NUT"
revision = "cb7f23ec59bec0d61b19c56cd88cee3d0cc1870c"
[[projects]]
digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
name = "github.com/sergi/go-diff"
packages = ["diffmatchpatch"]
pruneopts = "NUT"
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
version = "v1.0.0"
[[projects]]
digest = "1:1f0b284a6858827de4c27c66b49b2b25df3e16b031c2b57b7892273131e7dd2b"
name = "github.com/smartystreets/assertions"
packages = [
".",
"internal/go-render/render",
"internal/oglematchers"
"internal/oglematchers",
]
pruneopts = "NUT"
revision = "7678a5452ebea5b7090a6b163f844c133f523da2"
version = "1.8.3"
[[projects]]
digest = "1:7efd0b2309cdd6468029fa30c808c50a820c9344df07e1a4bbdaf18f282907aa"
name = "github.com/smartystreets/goconvey"
packages = [
"convey",
"convey/gotest",
"convey/reporting"
"convey/reporting",
]
pruneopts = "NUT"
revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
version = "1.6.3"
[[projects]]
branch = "master"
digest = "1:a66add8dd963bfc72649017c1b321198f596cb4958cb1a11ff91a1be8691020b"
name = "github.com/teris-io/shortid"
packages = ["."]
pruneopts = "NUT"
revision = "771a37caa5cf0c81f585d7b6df4dfc77e0615b5c"
[[projects]]
digest = "1:3d48c38e0eca8c66df62379c5ae7a83fb5cd839b94f241354c07ba077da7bc45"
name = "github.com/uber/jaeger-client-go"
packages = [
".",
@ -479,45 +596,55 @@
"thrift-gen/jaeger",
"thrift-gen/sampling",
"thrift-gen/zipkincore",
"utils"
"utils",
]
pruneopts = "NUT"
revision = "b043381d944715b469fd6b37addfd30145ca1758"
version = "v2.14.0"
[[projects]]
digest = "1:0f09db8429e19d57c8346ad76fbbc679341fa86073d3b8fb5ac919f0357d8f4c"
name = "github.com/uber/jaeger-lib"
packages = ["metrics"]
pruneopts = "NUT"
revision = "ed3a127ec5fef7ae9ea95b01b542c47fbd999ce5"
version = "v1.5.0"
[[projects]]
digest = "1:4c7d12ad3ef47bb03892a52e2609dc9a9cff93136ca9c7d31c00b79fcbc23c7b"
name = "github.com/yudai/gojsondiff"
packages = [
".",
"formatter"
"formatter",
]
pruneopts = "NUT"
revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
version = "1.0.0"
[[projects]]
branch = "master"
digest = "1:e50cbf8eba568d59b71e08c22c2a77809ed4646ae06ef4abb32b3d3d3fdb1a77"
name = "github.com/yudai/golcs"
packages = ["."]
pruneopts = "NUT"
revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
[[projects]]
branch = "master"
digest = "1:758f363e0dff33cf00b234be2efb12f919d79b42d5ae3909ff9eb69ef2c3cca5"
name = "golang.org/x/crypto"
packages = [
"ed25519",
"ed25519/internal/edwards25519",
"md4",
"pbkdf2"
"pbkdf2",
]
pruneopts = "NUT"
revision = "1a580b3eff7814fc9b40602fd35256c63b50f491"
[[projects]]
branch = "master"
digest = "1:0b3fee9c4472022a0982ee0d81e08b3cc3e595f50befd7a4b358b48540d9d8c5"
name = "golang.org/x/net"
packages = [
"context",
@ -527,35 +654,43 @@
"http2/hpack",
"idna",
"internal/timeseries",
"trace"
"trace",
]
pruneopts = "NUT"
revision = "2491c5de3490fced2f6cff376127c667efeed857"
[[projects]]
branch = "master"
digest = "1:46bd4e66bfce5e77f08fc2e8dcacc3676e679241ce83d9c150ff0397d686dd44"
name = "golang.org/x/oauth2"
packages = [
".",
"google",
"internal",
"jws",
"jwt"
"jwt",
]
pruneopts = "NUT"
revision = "cdc340f7c179dbbfa4afd43b7614e8fcadde4269"
[[projects]]
branch = "master"
digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
name = "golang.org/x/sync"
packages = ["errgroup"]
pruneopts = "NUT"
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
branch = "master"
digest = "1:ec21c5bf0572488865b93e30ffd9132afbf85bec0b20c2d6cbcf349cf2031ed5"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "NUT"
revision = "7c87d13f8e835d2fb3a70a2912c811ed0c1d241b"
[[projects]]
digest = "1:e7071ed636b5422cc51c0e3a6cebc229d6c9fffc528814b519a980641422d619"
name = "golang.org/x/text"
packages = [
"collate",
@ -571,12 +706,14 @@
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable"
"unicode/rangetable",
]
pruneopts = "NUT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
digest = "1:dbd5568923513ee74aa626d027e2a8a352cf8f35df41d19f4e34491d1858c38b"
name = "google.golang.org/appengine"
packages = [
".",
@ -589,18 +726,22 @@
"internal/modules",
"internal/remote_api",
"internal/urlfetch",
"urlfetch"
"urlfetch",
]
pruneopts = "NUT"
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:3c24554c312721e98fa6b76403e7100cf974eb46b1255ea7fc6471db9a9ce498"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
pruneopts = "NUT"
revision = "7bb2a897381c9c5ab2aeb8614f758d7766af68ff"
[[projects]]
digest = "1:840b77b6eb539b830bb760b6e30b688ed2ff484bd83466fce2395835ed9367fe"
name = "google.golang.org/grpc"
packages = [
".",
@ -627,78 +768,177 @@
"stats",
"status",
"tap",
"transport"
"transport",
]
pruneopts = "NUT"
revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34"
version = "v1.11.1"
[[projects]]
branch = "v3"
digest = "1:1244a9b3856f70d5ffb74bbfd780fc9d47f93f2049fa265c6fb602878f507bf8"
name = "gopkg.in/alexcesaro/quotedprintable.v3"
packages = ["."]
pruneopts = "NUT"
revision = "2caba252f4dc53eaf6b553000885530023f54623"
[[projects]]
digest = "1:aea6e9483c167cc6fdf1274c442558c5dda8fd3373372be04d98c79100868da1"
name = "gopkg.in/asn1-ber.v1"
packages = ["."]
pruneopts = "NUT"
revision = "379148ca0225df7a432012b8df0355c2a2063ac0"
version = "v1.2"
[[projects]]
digest = "1:24bfc2e8bf971485cb5ba0f0e5b08a1b806cca5828134df76b32d1ea50f2ab49"
name = "gopkg.in/bufio.v1"
packages = ["."]
pruneopts = "NUT"
revision = "567b2bfa514e796916c4747494d6ff5132a1dfce"
version = "v1"
[[projects]]
digest = "1:e05711632e1515319b014e8fe4cbe1d30ab024c473403f60cf0fdeb4c586a474"
name = "gopkg.in/ini.v1"
packages = ["."]
pruneopts = "NUT"
revision = "6529cf7c58879c08d927016dde4477f18a0634cb"
version = "v1.36.0"
[[projects]]
digest = "1:c847b7fea4c7e6db5281a37dffc4620cb78c1227403a79e5aa290db517657ac1"
name = "gopkg.in/ldap.v3"
packages = ["."]
pruneopts = "NUT"
revision = "5c2c0f997205c29de14cb6c35996370c2c5dfab1"
version = "v3"
[[projects]]
digest = "1:3b0cf3a465fd07f76e5fc1a9d0783c662dac0de9fc73d713ebe162768fd87b5f"
name = "gopkg.in/macaron.v1"
packages = ["."]
pruneopts = "NUT"
revision = "c1be95e6d21e769e44e1ec33cec9da5837861c10"
version = "v1.3.1"
[[projects]]
branch = "v2"
digest = "1:d52332f9e9f2c6343652e13aa3fd40cfd03353520c9a48d90f21215d3012d50f"
name = "gopkg.in/mail.v2"
packages = ["."]
pruneopts = "NUT"
revision = "5bc5c8bb07bd8d2803831fbaf8cbd630fcde2c68"
[[projects]]
digest = "1:00126f697efdcab42f07c89ac8bf0095fb2328aef6464e070055154088cea859"
name = "gopkg.in/redis.v2"
packages = ["."]
pruneopts = "NUT"
revision = "e6179049628164864e6e84e973cfb56335748dea"
version = "v2.3.2"
[[projects]]
digest = "1:a50fabe7a46692dc7c656310add3d517abe7914df02afd151ef84da884605dc8"
name = "gopkg.in/square/go-jose.v2"
packages = [
".",
"cipher",
"json"
"json",
]
pruneopts = "NUT"
revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8"
version = "v2.1.9"
[[projects]]
branch = "v2"
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "NUT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "88f0eb826b9c154ba46ea3bb64767707d86db75449ec75199eb2b8cf2b337fd4"
input-imports = [
"github.com/BurntSushi/toml",
"github.com/Unknwon/com",
"github.com/VividCortex/mysqlerr",
"github.com/aws/aws-sdk-go/aws",
"github.com/aws/aws-sdk-go/aws/awserr",
"github.com/aws/aws-sdk-go/aws/awsutil",
"github.com/aws/aws-sdk-go/aws/credentials",
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds",
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds",
"github.com/aws/aws-sdk-go/aws/defaults",
"github.com/aws/aws-sdk-go/aws/ec2metadata",
"github.com/aws/aws-sdk-go/aws/endpoints",
"github.com/aws/aws-sdk-go/aws/request",
"github.com/aws/aws-sdk-go/aws/session",
"github.com/aws/aws-sdk-go/service/cloudwatch",
"github.com/aws/aws-sdk-go/service/ec2",
"github.com/aws/aws-sdk-go/service/ec2/ec2iface",
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi",
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface",
"github.com/aws/aws-sdk-go/service/s3",
"github.com/aws/aws-sdk-go/service/sts",
"github.com/benbjohnson/clock",
"github.com/bmizerany/assert",
"github.com/codegangsta/cli",
"github.com/davecgh/go-spew/spew",
"github.com/denisenkom/go-mssqldb",
"github.com/facebookgo/inject",
"github.com/fatih/color",
"github.com/go-macaron/binding",
"github.com/go-macaron/gzip",
"github.com/go-macaron/session",
"github.com/go-macaron/session/memcache",
"github.com/go-macaron/session/postgres",
"github.com/go-macaron/session/redis",
"github.com/go-sql-driver/mysql",
"github.com/go-stack/stack",
"github.com/go-xorm/core",
"github.com/go-xorm/xorm",
"github.com/gorilla/websocket",
"github.com/gosimple/slug",
"github.com/grafana/grafana-plugin-model/go/datasource",
"github.com/grafana/grafana-plugin-model/go/renderer",
"github.com/hashicorp/go-hclog",
"github.com/hashicorp/go-plugin",
"github.com/hashicorp/go-version",
"github.com/inconshreveable/log15",
"github.com/lib/pq",
"github.com/mattn/go-isatty",
"github.com/mattn/go-sqlite3",
"github.com/opentracing/opentracing-go",
"github.com/opentracing/opentracing-go/ext",
"github.com/opentracing/opentracing-go/log",
"github.com/patrickmn/go-cache",
"github.com/pkg/errors",
"github.com/prometheus/client_golang/api",
"github.com/prometheus/client_golang/api/prometheus/v1",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/prometheus/client_model/go",
"github.com/prometheus/common/expfmt",
"github.com/prometheus/common/model",
"github.com/smartystreets/goconvey/convey",
"github.com/teris-io/shortid",
"github.com/uber/jaeger-client-go/config",
"github.com/yudai/gojsondiff",
"github.com/yudai/gojsondiff/formatter",
"golang.org/x/net/context/ctxhttp",
"golang.org/x/oauth2",
"golang.org/x/oauth2/google",
"golang.org/x/oauth2/jwt",
"golang.org/x/sync/errgroup",
"gopkg.in/ini.v1",
"gopkg.in/ldap.v3",
"gopkg.in/macaron.v1",
"gopkg.in/mail.v2",
"gopkg.in/square/go-jose.v2",
"gopkg.in/yaml.v2",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -81,11 +81,15 @@ ignored = [
[[constraint]]
name = "github.com/go-xorm/core"
version = "=0.5.7"
version = "=0.6.2"
[[override]]
name = "github.com/go-xorm/builder"
version = "=0.3.4"
[[constraint]]
name = "github.com/go-xorm/xorm"
version = "=0.6.4"
version = "=0.7.1"
[[constraint]]
name = "github.com/gorilla/websocket"

View File

@ -1,3 +1,3 @@
FROM prom/prometheus:v2.2.0
FROM prom/prometheus:v2.7.2
ADD prometheus.yml /etc/prometheus/
ADD alert.rules /etc/prometheus/

View File

@ -1,4 +1,4 @@
{
"stable": "6.0.0",
"testing": "6.0.0"
"stable": "6.0.1",
"testing": "6.0.1"
}

View File

@ -123,10 +123,10 @@
},
"scripts": {
"dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js",
"start": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme",
"start:hot": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --hot --theme",
"start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --hot",
"watch": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme -d watch,start",
"start": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --watchTheme",
"start:hot": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot --watchTheme",
"start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot",
"watch": "yarn start -d watch,start core:start --watchTheme ",
"build": "grunt build",
"test": "grunt test",
"tslint": "tslint -c tslint.json --project tsconfig.json",
@ -136,8 +136,11 @@
"storybook": "cd packages/grafana-ui && yarn storybook",
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
"gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --build",
"gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --release"
"gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build",
"gui:releasePrepare": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release",
"gui:publish": "cd packages/grafana-ui/dist && npm publish --access public",
"gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p",
"cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts"
},
"husky": {
"hooks": {
@ -169,7 +172,6 @@
"angular-native-dragdrop": "1.2.2",
"angular-route": "1.6.6",
"angular-sanitize": "1.6.6",
"ansicolor": "1.1.78",
"baron": "^3.0.3",
"brace": "^0.10.0",
"classnames": "^2.2.6",

View File

@ -54,34 +54,34 @@ $orange: ${theme.colors.orange};
$purple: ${theme.colors.purple};
$variable: ${theme.colors.variable};
$brand-primary: $orange;
$brand-success: $green-base;
$brand-warning: $brand-primary;
$brand-danger: $red-base;
$brand-primary: ${theme.colors.brandPrimary};
$brand-success: ${theme.colors.brandSuccess};
$brand-warning: ${theme.colors.brandWarning};
$brand-danger: ${theme.colors.brandDanger};
$query-red: $red-base;
$query-green: #74e680;
$query-purple: #fe85fc;
$query-keyword: #66d9ef;
$query-orange: $orange;
$query-red: ${theme.colors.queryRed};
$query-green: ${theme.colors.queryGreen};
$query-purple: ${theme.colors.queryPurple};
$query-orange: ${theme.colors.orange};
$query-keyword: ${theme.colors.queryKeyword};
// Status colors
// -------------------------
$online: $green-base;
$warn: #f79520;
$critical: $red-base;
$online: ${theme.colors.online};
$warn: ${theme.colors.warn};
$critical: ${theme.colors.critical};
// Scaffolding
// -------------------------
$body-bg: ${theme.colors.bodyBg};
$page-bg: ${theme.colors.pageBg};
$body-color: $gray-4;
$text-color: $gray-4;
$text-color-strong: $white;
$text-color-weak: $gray-2;
$text-color-faint: $dark-10;
$text-color-emphasis: $gray-5;
$body-color: ${theme.colors.body};
$text-color: ${theme.colors.text};
$text-color-strong: ${theme.colors.textStrong};
$text-color-weak: ${theme.colors.textWeak};
$text-color-faint: ${theme.colors.textFaint};
$text-color-emphasis: ${theme.colors.textEmphasis};
$text-shadow-faint: 1px 1px 4px rgb(45, 45, 45);
$textShadow: none;
@ -99,14 +99,14 @@ $edit-gradient: linear-gradient(180deg, $dark-2 50%, $input-black);
// Links
// -------------------------
$link-color: darken($white, 11%);
$link-color-disabled: darken($link-color, 30%);
$link-hover-color: $white;
$external-link-color: $blue-light;
$link-color: ${theme.colors.link};
$link-color-disabled: ${theme.colors.linkDisabled};
$link-hover-color: ${theme.colors.linkHover};
$external-link-color: ${theme.colors.linkExternal};
// Typography
// -------------------------
$headings-color: darken($white, 11%);
$headings-color: ${theme.colors.headingColor};
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;

View File

@ -46,34 +46,34 @@ $orange: ${theme.colors.orange};
$purple: ${theme.colors.purple};
$variable: ${theme.colors.variable};
$brand-primary: $orange;
$brand-success: $green-base;
$brand-warning: $orange;
$brand-danger: $red-base;
$brand-primary: ${theme.colors.brandPrimary};
$brand-success: ${theme.colors.brandSuccess};
$brand-warning: ${theme.colors.brandWarning};
$brand-danger: ${theme.colors.brandDanger};
$query-red: $red-base;
$query-green: $green-base;
$query-purple: $purple;
$query-orange: $orange;
$query-keyword: $blue-base;
$query-red: ${theme.colors.queryRed};
$query-green: ${theme.colors.queryGreen};
$query-purple: ${theme.colors.queryPurple};
$query-orange: ${theme.colors.orange};
$query-keyword: ${theme.colors.queryKeyword};
// Status colors
// -------------------------
$online: $green-shade;
$warn: #f79520;
$critical: $red-shade;
$online: ${theme.colors.online};
$warn: ${theme.colors.warn};
$critical: ${theme.colors.critical};
// Scaffolding
// -------------------------
$body-bg: ${theme.colors.bodyBg};
$page-bg: ${theme.colors.pageBg};
$body-color: $gray-1;
$text-color: $gray-1;
$text-color-strong: $dark-1;
$text-color-weak: $gray-2;
$text-color-faint: $gray-4;
$text-color-emphasis: $dark-2;
$body-color: ${theme.colors.body};
$text-color: ${theme.colors.text};
$text-color-strong: ${theme.colors.textStrong};
$text-color-weak: ${theme.colors.textWeak};
$text-color-faint: ${theme.colors.textFaint};
$text-color-emphasis: ${theme.colors.textEmphasis};
$text-shadow-faint: none;
@ -85,14 +85,14 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
// Links
// -------------------------
$link-color: $gray-1;
$link-color-disabled: lighten($link-color, 30%);
$link-hover-color: darken($link-color, 20%);
$external-link-color: $blue-shade;
$link-color: ${theme.colors.link};
$link-color-disabled: ${theme.colors.linkDisabled};
$link-hover-color: ${theme.colors.linkHover};
$external-link-color: ${theme.colors.linkExternal};
// Typography
// -------------------------
$headings-color: $text-color;
$headings-color: ${theme.colors.headingColor};
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;

View File

@ -46,6 +46,10 @@ const darkTheme: GrafanaTheme = {
colors: {
...basicColors,
inputBlack: '#09090b',
brandPrimary: basicColors.orange,
brandSuccess: basicColors.greenBase,
brandWarning: basicColors.orange,
brandDanger: basicColors.redBase,
queryRed: basicColors.redBase,
queryGreen: '#74e680',
queryPurple: '#fe85fc',
@ -56,16 +60,16 @@ const darkTheme: GrafanaTheme = {
critical: basicColors.redBase,
bodyBg: basicColors.dark2,
pageBg: basicColors.dark2,
bodyColor: basicColors.gray4,
textColor: basicColors.gray4,
textColorStrong: basicColors.white,
textColorWeak: basicColors.gray2,
textColorEmphasis: basicColors.gray5,
textColorFaint: basicColors.dark5,
linkColor: new tinycolor(basicColors.white).darken(11).toString(),
linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(),
linkColorHover: basicColors.white,
linkColorExternal: basicColors.blue,
body: basicColors.gray4,
text: basicColors.gray4,
textStrong: basicColors.white,
textWeak: basicColors.gray2,
textEmphasis: basicColors.gray5,
textFaint: basicColors.dark5,
link: new tinycolor(basicColors.white).darken(11).toString(),
linkDisabled: new tinycolor(basicColors.white).darken(11).toString(),
linkHover: basicColors.white,
linkExternal: basicColors.blue,
headingColor: new tinycolor(basicColors.white).darken(11).toString(),
},
background: {

View File

@ -47,26 +47,30 @@ const lightTheme: GrafanaTheme = {
...basicColors,
variable: basicColors.blue,
inputBlack: '#09090b',
queryRed: basicColors.red,
brandPrimary: basicColors.orange,
brandSuccess: basicColors.greenBase,
brandWarning: basicColors.orange,
brandDanger: basicColors.redBase,
queryRed: basicColors.redBase,
queryGreen: basicColors.greenBase,
queryPurple: basicColors.purple,
queryKeyword: basicColors.blue,
queryKeyword: basicColors.blueBase,
queryOrange: basicColors.orange,
online: basicColors.greenShade,
warn: '#f79520',
critical: basicColors.redShade,
bodyBg: basicColors.gray7,
pageBg: basicColors.gray7,
bodyColor: basicColors.gray1,
textColor: basicColors.gray1,
textColorStrong: basicColors.dark2,
textColorWeak: basicColors.gray2,
textColorEmphasis: basicColors.gray5,
textColorFaint: basicColors.dark4,
linkColor: basicColors.gray1,
linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(),
linkColorExternal: basicColors.blueLight,
body: basicColors.gray1,
text: basicColors.gray1,
textStrong: basicColors.dark2,
textWeak: basicColors.gray2,
textEmphasis: basicColors.gray5,
textFaint: basicColors.dark4,
link: basicColors.gray1,
linkDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
linkHover: new tinycolor(basicColors.gray1).darken(20).toString(),
linkExternal: basicColors.blueLight,
headingColor: basicColors.gray1,
},
background: {

View File

@ -1,8 +1,9 @@
import { ComponentClass } from 'react';
import { TimeSeries, LoadingState, TableData } from './data';
import { TimeRange } from './time';
import { ScopedVars } from './datasource';
export type InterpolateFunction = (value: string, format?: string | Function) => string;
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
export interface PanelProps<T = any> {
panelData: PanelData;

View File

@ -113,25 +113,33 @@ export interface GrafanaTheme extends GrafanaThemeCommons {
queryPurple: string;
queryKeyword: string;
queryOrange: string;
brandPrimary: string;
brandSuccess: string;
brandWarning: string;
brandDanger: string;
// Status colors
online: string;
warn: string;
critical: string;
// Link colors
link: string;
linkDisabled: string;
linkHover: string;
linkExternal: string;
// Text colors
body: string;
text: string;
textStrong: string;
textWeak: string;
textFaint: string;
textEmphasis: string;
// TODO: move to background section
bodyBg: string;
pageBg: string;
bodyColor: string;
textColor: string;
textColorStrong: string;
textColorWeak: string;
textColorFaint: string;
textColorEmphasis: string;
linkColor: string;
linkColorDisabled: string;
linkColorHover: string;
linkColorExternal: string;
headingColor: string;
};
}

View File

@ -137,7 +137,7 @@ export const getCategories = (): ValueFormatCategory[] => [
formats: [
{ name: 'packets/sec', id: 'pps', fn: decimalSIPrefix('pps') },
{ name: 'bits/sec', id: 'bps', fn: decimalSIPrefix('bps') },
{ name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('B/s') },
{ name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('Bs') },
{ name: 'kilobytes/sec', id: 'KBs', fn: decimalSIPrefix('Bs', 1) },
{ name: 'kilobits/sec', id: 'Kbits', fn: decimalSIPrefix('bps', 1) },
{ name: 'megabytes/sec', id: 'MBs', fn: decimalSIPrefix('Bs', 2) },

View File

@ -488,6 +488,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.Resto
saveCmd.Dashboard.Set("version", dash.Version)
saveCmd.Dashboard.Set("uid", dash.Uid)
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
saveCmd.FolderId = dash.FolderId
return hs.PostDashboard(c, saveCmd)
}

View File

@ -810,6 +810,93 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
})
})
Convey("Given dashboard in folder being restored should restore to folder", t, func() {
fakeDash := m.NewDashboard("Child dash")
fakeDash.Id = 2
fakeDash.FolderId = 1
fakeDash.HasAcl = false
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{
DashboardId: 2,
Version: 1,
Data: fakeDash.Data,
}
return nil
})
mock := &dashboards.FakeDashboardService{
SaveDashboardResult: &m.Dashboard{
Id: 2,
Uid: "uid",
Title: "Dash",
Slug: "dash",
Version: 1,
},
}
cmd := dtos.RestoreDashboardVersionCommand{
Version: 1,
}
restoreDashboardVersionScenario("When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) {
CallRestoreDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 200)
dto := mock.SavedDashboards[0]
So(dto.Dashboard.FolderId, ShouldEqual, 1)
So(dto.Dashboard.Title, ShouldEqual, "Child dash")
So(dto.Message, ShouldEqual, "Restored from version 1")
})
})
Convey("Given dashboard in general folder being restored should restore to general folder", t, func() {
fakeDash := m.NewDashboard("Child dash")
fakeDash.Id = 2
fakeDash.HasAcl = false
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{
DashboardId: 2,
Version: 1,
Data: fakeDash.Data,
}
return nil
})
mock := &dashboards.FakeDashboardService{
SaveDashboardResult: &m.Dashboard{
Id: 2,
Uid: "uid",
Title: "Dash",
Slug: "dash",
Version: 1,
},
}
cmd := dtos.RestoreDashboardVersionCommand{
Version: 1,
}
restoreDashboardVersionScenario("When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) {
CallRestoreDashboardVersion(sc)
So(sc.resp.Code, ShouldEqual, 200)
dto := mock.SavedDashboards[0]
So(dto.Dashboard.FolderId, ShouldEqual, 0)
So(dto.Dashboard.Title, ShouldEqual, "Child dash")
So(dto.Message, ShouldEqual, "Restored from version 1")
})
})
}
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
@ -871,6 +958,10 @@ func CallPostDashboard(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func CallRestoreDashboardVersion(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) {
CallPostDashboard(sc)
@ -928,6 +1019,39 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal
})
}
func restoreDashboardVersionScenario(desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
hs := HTTPServer{
Bus: bus.GetBus(),
}
sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
sc.context = c
sc.context.SignedInUser = &m.SignedInUser{
OrgId: TestOrgID,
UserId: TestUserID,
}
sc.context.OrgRole = m.ROLE_ADMIN
return hs.RestoreDashboardVersion(c, cmd)
})
origNewDashboardService := dashboards.NewService
dashboards.MockDashboardService(mock)
sc.m.Post(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewService = origNewDashboardService
}()
fn(sc)
})
}
func (sc *scenarioContext) ToJSON() *simplejson.Json {
var result *simplejson.Json
err := json.NewDecoder(sc.resp.Body).Decode(&result)

View File

@ -25,6 +25,7 @@ var filters map[string]log15.Lvl
func init() {
loggersToClose = make([]DisposableHandler, 0)
loggersToReload = make([]ReloadableHandler, 0)
filters = map[string]log15.Lvl{}
Root = log15.Root()
Root.SetHandler(log15.DiscardHandler())
}
@ -197,7 +198,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
// Log level.
_, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg)
filters := getFilters(util.SplitString(sec.Key("filters").String()))
modeFilters := getFilters(util.SplitString(sec.Key("filters").String()))
format := getLogFormat(sec.Key("format").MustString(""))
var handler log15.Handler
@ -230,12 +231,18 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
}
for key, value := range defaultFilters {
if _, exist := modeFilters[key]; !exist {
modeFilters[key] = value
}
}
for key, value := range modeFilters {
if _, exist := filters[key]; !exist {
filters[key] = value
}
}
handler = LogFilterHandler(level, filters, handler)
handler = LogFilterHandler(level, modeFilters, handler)
handlers = append(handlers, handler)
}

View File

@ -18,6 +18,7 @@ import (
type ILdapConn interface {
Bind(username, password string) error
UnauthenticatedBind(username string) error
Search(*ldap.SearchRequest) (*ldap.SearchResult, error)
StartTLS(*tls.Config) error
Close()
@ -259,7 +260,17 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
bindPath = fmt.Sprintf(a.server.BindDN, username)
}
if err := a.conn.Bind(bindPath, userPassword); err != nil {
bindFn := func() error {
return a.conn.Bind(bindPath, userPassword)
}
if userPassword == "" {
bindFn = func() error {
return a.conn.UnauthenticatedBind(bindPath)
}
}
if err := bindFn(); err != nil {
a.log.Info("Initial bind failed", "error", err)
if ldapErr, ok := err.(*ldap.Error); ok {

View File

@ -13,6 +13,70 @@ import (
)
func TestLdapAuther(t *testing.T) {
Convey("initialBind", t, func() {
Convey("Given bind dn and password configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
err := ldapAuther.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(ldapAuther.requireSecondBind, ShouldBeTrue)
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{
BindDN: "cn=%s,o=users,dc=grafana,dc=org",
},
}
err := ldapAuther.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(ldapAuther.requireSecondBind, ShouldBeFalse)
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "pwd")
})
Convey("Given empty bind dn and password", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{},
}
err := ldapAuther.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(ldapAuther.requireSecondBind, ShouldBeTrue)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
Convey("When translating ldap user to grafana user", t, func() {
@ -365,12 +429,26 @@ func TestLdapAuther(t *testing.T) {
}
type mockLdapConn struct {
result *ldap.SearchResult
searchCalled bool
searchAttributes []string
result *ldap.SearchResult
searchCalled bool
searchAttributes []string
bindProvider func(username, password string) error
unauthenticatedBindProvider func(username string) error
}
func (c *mockLdapConn) Bind(username, password string) error {
if c.bindProvider != nil {
return c.bindProvider(username, password)
}
return nil
}
func (c *mockLdapConn) UnauthenticatedBind(username string) error {
if c.unauthenticatedBindProvider != nil {
return c.unauthenticatedBindProvider(username)
}
return nil
}

View File

@ -138,7 +138,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
return err
}
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.Uid, ref.Slug, context.Rule.OrgId, context.Rule.PanelId)
result, err := n.renderService.Render(context.Ctx, renderOpts)
if err != nil {

View File

@ -111,57 +111,20 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
json, _ := bodyJSON.MarshalJSON()
content_type := "application/json"
var body []byte
if embeddedImage {
var b bytes.Buffer
w := multipart.NewWriter(&b)
f, err := os.Open(evalContext.ImageOnDiskPath)
if err != nil {
this.log.Error("Can't open graph file", err)
return err
}
defer f.Close()
fw, err := w.CreateFormField("payload_json")
if err != nil {
return err
}
if _, err = fw.Write([]byte(string(json))); err != nil {
return err
}
fw, err = w.CreateFormFile("file", "graph.png")
if err != nil {
return err
}
if _, err = io.Copy(fw, f); err != nil {
return err
}
w.Close()
body = b.Bytes()
content_type = w.FormDataContentType()
} else {
body = json
}
cmd := &m.SendWebhookSync{
Url: this.WebhookURL,
Body: string(body),
HttpMethod: "POST",
ContentType: content_type,
ContentType: "application/json",
}
if !embeddedImage {
cmd.Body = string(json)
} else {
err := this.embedImage(cmd, evalContext.ImageOnDiskPath, json)
if err != nil {
this.log.Error("failed to embed image", "error", err)
return err
}
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
@ -171,3 +134,45 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
func (this *DiscordNotifier) embedImage(cmd *m.SendWebhookSync, imagePath string, existingJSONBody []byte) error {
f, err := os.Open(imagePath)
defer f.Close()
if err != nil {
if os.IsNotExist(err) {
cmd.Body = string(existingJSONBody)
return nil
}
if !os.IsNotExist(err) {
return err
}
}
var b bytes.Buffer
w := multipart.NewWriter(&b)
fw, err := w.CreateFormField("payload_json")
if err != nil {
return err
}
if _, err = fw.Write([]byte(string(existingJSONBody))); err != nil {
return err
}
fw, err = w.CreateFormFile("file", "graph.png")
if err != nil {
return err
}
if _, err = io.Copy(fw, f); err != nil {
return err
}
w.Close()
cmd.Body = string(b.Bytes())
cmd.ContentType = w.FormDataContentType()
return nil
}

View File

@ -36,7 +36,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
defer middleware.RemoveRenderAuthKey(renderKey)
phantomDebugArg := "--debug=false"
if log.GetLogLevelFor("renderer") >= log.LvlDebug {
if log.GetLogLevelFor("rendering") >= log.LvlDebug {
phantomDebugArg = "--debug=true"
}
@ -64,13 +64,26 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...)
cmd.Stderr = cmd.Stdout
timezone := ""
if opts.Timezone != "" {
timezone = isoTimeOffsetToPosixTz(opts.Timezone)
baseEnviron := os.Environ()
cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(opts.Timezone))
cmd.Env = appendEnviron(baseEnviron, "TZ", timezone)
}
rs.log.Debug("executing Phantomjs", "binPath", binPath, "cmdArgs", cmdArgs, "timezone", timezone)
out, err := cmd.Output()
if out != nil {
rs.log.Debug("Phantomjs output", "out", string(out))
}
if err != nil {
rs.log.Debug("Phantomjs error", "error", err)
}
// check for timeout first
if commandCtx.Err() == context.DeadlineExceeded {
rs.log.Info("Rendering timed out")
@ -82,8 +95,6 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
return nil, err
}
rs.log.Debug("Phantomjs output", "out", string(out))
rs.log.Debug("Image rendered", "path", pngPath)
return &RenderResult{FilePath: pngPath}, nil
}

View File

@ -309,7 +309,9 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
params = append(params, v)
}
res, err := sess.Exec(buffer.String(), params...)
sqlOrArgs := append([]interface{}{buffer.String()}, params...)
res, err := sess.Exec(sqlOrArgs...)
if err != nil {
return err
}

View File

@ -258,11 +258,15 @@ func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
queryParams = []interface{}{params.DashboardId, params.PanelId, params.OrgId}
}
if _, err := sess.Exec(annoTagSql, queryParams...); err != nil {
sqlOrArgs := append([]interface{}{annoTagSql}, queryParams...)
if _, err := sess.Exec(sqlOrArgs...); err != nil {
return err
}
if _, err := sess.Exec(sql, queryParams...); err != nil {
sqlOrArgs = append([]interface{}{sql}, queryParams...)
if _, err := sess.Exec(sqlOrArgs...); err != nil {
return err
}

View File

@ -51,7 +51,7 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error {
dashboard_version.message,
dashboard_version.data,`+
dialect.Quote("user")+`.login as created_by`).
Join("LEFT", "user", `dashboard_version.created_by = `+dialect.Quote("user")+`.id`).
Join("LEFT", dialect.Quote("user"), `dashboard_version.created_by = `+dialect.Quote("user")+`.id`).
Join("LEFT", "dashboard", `dashboard.id = dashboard_version.dashboard_id`).
Where("dashboard_version.dashboard_id=? AND dashboard.org_id=?", query.DashboardId, query.OrgId).
OrderBy("dashboard_version.version DESC").
@ -102,7 +102,8 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error {
if len(versionIdsToDelete) > 0 {
deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)`
expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...)
sqlOrArgs := append([]interface{}{deleteExpiredSql}, versionIdsToDelete...)
expiredResponse, err := sess.Exec(sqlOrArgs...)
if err != nil {
return err
}

View File

@ -98,7 +98,7 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error {
query.Result = make([]*m.OrgUserDTO, 0)
sess := x.Table("org_user")
sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
sess.Join("INNER", x.Dialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
whereConditions := make([]string, 0)
whereParams := make([]interface{}, 0)

View File

@ -280,7 +280,7 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
func GetTeamMembers(query *m.GetTeamMembersQuery) error {
query.Result = make([]*m.TeamMemberDTO, 0)
sess := x.Table("team_member")
sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
sess.Join("INNER", x.Dialect().Quote("user"), fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
if query.OrgId != 0 {
sess.Where("team_member.org_id=?", query.OrgId)
}

View File

@ -208,7 +208,7 @@ func TestUserDataAccess(t *testing.T) {
func GetOrgUsersForTest(query *m.GetOrgUsersQuery) error {
query.Result = make([]*m.OrgUserDTO, 0)
sess := x.Table("org_user")
sess.Join("LEFT ", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
sess.Join("LEFT ", x.Dialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("org_user.org_id=?", query.OrgId)
sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role")

View File

@ -27,6 +27,7 @@ export class HelpCtrl {
{ keys: ['d', 'C'], description: 'Collapse all rows' },
{ keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' },
{ keys: ['mod+o'], description: 'Toggle shared graph crosshair' },
{ keys: ['d', 'l'], description: 'Toggle all panel legends' },
],
'Focused Panel': [
{ keys: ['e'], description: 'Toggle panel edit view' },

View File

@ -5,7 +5,7 @@ export class JsonEditorCtrl {
/** @ngInject */
constructor($scope) {
$scope.json = angular.toJson($scope.model.object, true);
$scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.contextSrv.isEditor;
$scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.model.canUpdate;
$scope.canCopy = $scope.model.enableCopy;
$scope.update = () => {

View File

@ -256,6 +256,11 @@ export class KeybindingSrv {
}
});
// toggle all panel legends
this.bind('d l', () => {
dashboard.toggleLegendsForAll();
});
// collapse all rows
this.bind('d shift+c', () => {
dashboard.collapseRows();

View File

@ -17,7 +17,7 @@
</div>
<div class="gf-form max-width-11">
<label class="gf-form-label width-5">For</label>
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for"
<input type="text" class="gf-form-input max-width-6 gf-form-input--has-help-icon" ng-model="ctrl.alert.for"
spellcheck='false' placeholder="5m">
<info-popover mode="right-absolute">
If an alert rule has a configured For and the query violates the configured

View File

@ -100,7 +100,12 @@ export class AddPanelWidget extends React.Component<Props, State> {
const newPanel: any = {
type: panelPluginInfo.id,
title: 'Panel Title',
gridPos: { x: gridPos.x, y: gridPos.y, w: gridPos.w, h: gridPos.h },
gridPos: {
x: gridPos.x,
y: gridPos.y,
w: panelPluginInfo.defaults.gridPos.w,
h: panelPluginInfo.defaults.gridPos.h,
},
};
// apply panel template / defaults

View File

@ -0,0 +1,35 @@
import { PanelChrome } from './PanelChrome';
jest.mock('sass/_variables.generated.scss', () => ({
panelhorizontalpadding: 10,
panelVerticalPadding: 10,
}));
describe('PanelChrome', () => {
let chrome: PanelChrome;
beforeEach(() => {
chrome = new PanelChrome({
panel: {
scopedVars: {
aaa: { value: 'AAA', text: 'upperA' },
bbb: { value: 'BBB', text: 'upperB' },
},
},
dashboard: {},
plugin: {},
isFullscreen: false,
});
});
it('Should replace a panel variable', () => {
const out = chrome.replaceVariables('hello $aaa');
expect(out).toBe('hello AAA');
});
it('But it should prefer the local variable value', () => {
const extra = { aaa: { text: '???', value: 'XXX' } };
const out = chrome.replaceVariables('hello $aaa and $bbb', extra);
expect(out).toBe('hello XXX and BBB');
});
});

View File

@ -19,6 +19,7 @@ import { profiler } from 'app/core/profiler';
import { DashboardModel, PanelModel } from '../state';
import { PanelPlugin } from 'app/types';
import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
import { ScopedVars } from '@grafana/ui';
import variables from 'sass/_variables.generated.scss';
import templateSrv from 'app/features/templating/template_srv';
@ -85,8 +86,12 @@ export class PanelChrome extends PureComponent<Props, State> {
});
};
replaceVariables = (value: string, format?: string) => {
return templateSrv.replace(value, this.props.panel.scopedVars, format);
replaceVariables = (value: string, extraVars?: ScopedVars, format?: string) => {
let vars = this.props.panel.scopedVars;
if (extraVars) {
vars = vars ? { ...vars, ...extraVars } : extraVars;
}
return templateSrv.replace(value, vars, format);
};
onDataResponse = (dataQueryResponse: DataQueryResponse) => {

View File

@ -14,10 +14,10 @@ import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { FadeIn } from 'app/core/components/Animations/FadeIn';
// Types
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import { PanelModel } from '../state';
import { DashboardModel } from '../state';
import { PanelPlugin } from 'app/types/plugins';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { VizPickerSearch } from './VizPickerSearch';
interface Props {
panel: PanelModel;
@ -33,18 +33,19 @@ interface State {
isVizPickerOpen: boolean;
searchQuery: string;
scrollTop: number;
hasBeenFocused: boolean;
}
export class VisualizationTab extends PureComponent<Props, State> {
element: HTMLElement;
angularOptions: AngularComponent;
searchInput: HTMLElement;
constructor(props) {
super(props);
this.state = {
isVizPickerOpen: this.props.urlOpenVizPicker,
hasBeenFocused: false,
searchQuery: '',
scrollTop: 0,
};
@ -162,7 +163,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
this.props.updateLocation({ query: { openVizPicker: null }, partial: true });
}
this.setState({ isVizPickerOpen: false });
this.setState({ isVizPickerOpen: false, hasBeenFocused: false });
};
onSearchQueryChange = (value: string) => {
@ -173,23 +174,16 @@ export class VisualizationTab extends PureComponent<Props, State> {
renderToolbar = (): JSX.Element => {
const { plugin } = this.props;
const { searchQuery } = this.state;
const { isVizPickerOpen, searchQuery } = this.state;
if (this.state.isVizPickerOpen) {
if (isVizPickerOpen) {
return (
<>
<FilterInput
labelClassName="gf-form--has-input-icon"
inputClassName="gf-form-input width-13"
placeholder=""
onChange={this.onSearchQueryChange}
value={searchQuery}
ref={elem => elem && elem.focus()}
/>
<button className="btn btn-link toolbar__close" onClick={this.onCloseVizPicker}>
<i className="fa fa-chevron-up" />
</button>
</>
<VizPickerSearch
plugin={plugin}
searchQuery={searchQuery}
onChange={this.onSearchQueryChange}
onClose={this.onCloseVizPicker}
/>
);
} else {
return (

View File

@ -0,0 +1,33 @@
import React, { PureComponent } from 'react';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { PanelPlugin } from 'app/types';
interface Props {
plugin: PanelPlugin;
searchQuery: string;
onChange: (query: string) => void;
onClose: () => void;
}
export class VizPickerSearch extends PureComponent<Props> {
render() {
const { searchQuery, onChange, onClose } = this.props;
return (
<>
<FilterInput
labelClassName="gf-form--has-input-icon"
inputClassName="gf-form-input width-13"
placeholder=""
onChange={onChange}
value={searchQuery}
ref={element => element && element.focus()}
/>
<button className="btn btn-link toolbar__close" onClick={onClose}>
<i className="fa fa-chevron-up" />
</button>
</>
);
}
}

View File

@ -635,4 +635,32 @@ describe('DashboardModel', () => {
expect(saveModel.templating.list[0].filters[0].value).toBe('server 1');
});
});
describe('Given a dashboard with one panel legend on and two off', () => {
let model;
beforeEach(() => {
const data = {
panels: [
{ id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 2 }, legend: { show: true } },
{ id: 3, type: 'graph', gridPos: { x: 0, y: 4, w: 12, h: 2 }, legend: { show: false } },
{ id: 4, type: 'graph', gridPos: { x: 12, y: 4, w: 12, h: 2 }, legend: { show: false } },
],
};
model = new DashboardModel(data);
});
it('toggleLegendsForAll should toggle all legends on on first execution', () => {
model.toggleLegendsForAll();
const legendsOn = model.panels.filter(panel => panel.legend.show === true);
expect(legendsOn.length).toBe(3);
});
it('toggleLegendsForAll should toggle all legends off on second execution', () => {
model.toggleLegendsForAll();
model.toggleLegendsForAll();
const legendsOn = model.panels.filter(panel => panel.legend.show === true);
expect(legendsOn.length).toBe(0);
});
});
});

View File

@ -917,4 +917,20 @@ export class DashboardModel {
}
}
}
toggleLegendsForAll() {
const panelsWithLegends = this.panels.filter(panel => {
return panel.legend !== undefined && panel.legend !== null;
});
// determine if more panels are displaying legends or not
const onCount = panelsWithLegends.filter(panel => panel.legend.show).length;
const offCount = panelsWithLegends.length - onCount;
const panelLegendsOn = onCount >= offCount;
for (const panel of panelsWithLegends) {
panel.legend.show = !panelLegendsOn;
panel.render();
}
}
}

View File

@ -106,6 +106,7 @@ export class PanelModel {
events: Emitter;
cacheTimeout?: any;
cachedPluginOptions?: any;
legend?: { show: boolean };
constructor(model) {
this.events = new Emitter();

View File

@ -70,6 +70,7 @@ export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => {
updateHandler: (newPanel: PanelModel, oldPanel: PanelModel) => {
replacePanel(dashboard, newPanel, oldPanel);
},
canUpdate: dashboard.meta.canEdit,
enableCopy: true,
};

View File

@ -4,7 +4,7 @@
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-10">URL</span>
<input class="gf-form-input" type="text"
<input class="gf-form-input gf-form-input--has-help-icon" type="text"
ng-model='current.url' placeholder="{{suggestUrl}}"
bs-typeahead="getSuggestUrls" min-length="0"
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
@ -59,7 +59,7 @@
<div class="gf-form-inline" ng-if="current.access=='proxy'">
<div class="gf-form">
<span class="gf-form-label width-10">Whitelisted Cookies</span>
<bootstrap-tagsinput ng-model="current.jsonData.keepCookies" width-class="width-20" tagclass="label label-tag" placeholder="Add Name">
<bootstrap-tagsinput ng-model="current.jsonData.keepCookies" width-class="width-20 gf-form-input--has-help-icon" tagclass="label label-tag" placeholder="Add Name">
</bootstrap-tagsinput>
<info-popover mode="right-absolute">
Grafana Proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source.

View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import ansicolor from 'ansicolor';
import ansicolor from 'vendor/ansicolor/ansicolor';
interface Style {
[key: string]: string;

View File

@ -43,6 +43,9 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
this.props.onQueryChange(target);
this.props.onExecuteQuery();
},
onQueryChange: () => {
this.props.onQueryChange(target);
},
events: exploreEvents,
panel: { datasource, targets: [target] },
dashboard: {},

View File

@ -224,7 +224,7 @@ class MetricsPanelCtrl extends PanelCtrl {
items.push({
text: 'Explore',
click: 'ctrl.explore();',
icon: 'fa fa-fw fa-rocket',
icon: 'gicon gicon-explore',
shortcut: 'x',
});
}

View File

@ -7,6 +7,7 @@ import coreModule from '../../core/core_module';
import appEvents from 'app/core/app_events';
import locationUtil from 'app/core/utils/location_util';
import kbn from 'app/core/utils/kbn';
import { store } from 'app/store/store';
export class PlaylistSrv {
private cancelPromise: any;
@ -15,6 +16,8 @@ export class PlaylistSrv {
private interval: number;
private startUrl: string;
private numberOfLoops = 0;
private storeUnsub: () => void;
private validPlaylistUrl: string;
isPlaying: boolean;
/** @ngInject */
@ -39,15 +42,16 @@ export class PlaylistSrv {
const dash = this.dashboards[this.index];
const queryParams = this.$location.search();
const filteredParams = _.pickBy(queryParams, value => value !== null);
const nextDashboardUrl = locationUtil.stripBaseFromUrl(dash.url);
// this is done inside timeout to make sure digest happens after
// as this can be called from react
this.$timeout(() => {
const stripedUrl = locationUtil.stripBaseFromUrl(dash.url);
this.$location.url(stripedUrl + '?' + toUrlParams(filteredParams));
this.$location.url(nextDashboardUrl + '?' + toUrlParams(filteredParams));
});
this.index++;
this.validPlaylistUrl = nextDashboardUrl;
this.cancelPromise = this.$timeout(() => this.next(), this.interval);
}
@ -56,6 +60,15 @@ export class PlaylistSrv {
this.next();
}
// Detect url changes not caused by playlist srv and stop playlist
storeUpdated() {
const state = store.getState();
if (state.location.path !== this.validPlaylistUrl) {
this.stop();
}
}
start(playlistId) {
this.stop();
@ -63,6 +76,10 @@ export class PlaylistSrv {
this.index = 0;
this.isPlaying = true;
// setup location tracking
this.storeUnsub = store.subscribe(() => this.storeUpdated());
this.validPlaylistUrl = this.$location.path();
appEvents.emit('playlist-started');
return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
@ -85,6 +102,10 @@ export class PlaylistSrv {
this.index = 0;
this.isPlaying = false;
if (this.storeUnsub) {
this.storeUnsub();
}
if (this.cancelPromise) {
this.$timeout.cancel(this.cancelPromise);
}

View File

@ -1,4 +1,14 @@
import configureMockStore from 'redux-mock-store';
import { PlaylistSrv } from '../playlist_srv';
import { setStore } from 'app/store/store';
const mockStore = configureMockStore();
setStore(
mockStore({
location: {},
})
);
const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
@ -19,6 +29,7 @@ const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance<any, any> }
const mockLocation = {
url: jest.fn(),
search: () => ({}),
path: () => '/playlists/1',
};
const mockTimeout = jest.fn();
@ -96,4 +107,32 @@ describe('PlaylistSrv', () => {
expect(hrefMock).toHaveBeenCalledTimes(3);
expect(hrefMock).toHaveBeenLastCalledWith(initialUrl);
});
it('storeUpdated should stop playlist when navigating away', async () => {
await srv.start(1);
srv.storeUpdated();
expect(srv.isPlaying).toBe(false);
});
it('storeUpdated should not stop playlist when navigating to next dashboard', async () => {
await srv.start(1);
srv.next();
setStore(
mockStore({
location: {
path: 'dash2',
},
})
);
expect((srv as any).validPlaylistUrl).toBe('dash2');
srv.storeUpdated();
expect(srv.isPlaying).toBe(true);
});
});

View File

@ -25,7 +25,7 @@
</div>
<aside class="page-sidebar">
<section class="page-sidebar-section">
<section class="page-sidebar-section" ng-if="ctrl.model.info.version">
<h4>Version</h4>
<span>{{ctrl.model.info.version}}</span>
<div ng-show="ctrl.model.hasUpdate">
@ -54,7 +54,7 @@
</li>
</ul>
</section>
<section class="page-sidebar-section">
<section class="page-sidebar-section" ng-if="ctrl.model.info.links">
<h5>Links</h4>
<ul class="ui-list">
<li ng-repeat="link in ctrl.model.info.links">

View File

@ -1,7 +1,7 @@
import kbn from 'app/core/utils/kbn';
import _ from 'lodash';
import { variableRegex } from 'app/features/templating/variable';
import { TimeRange } from '@grafana/ui/src';
import { TimeRange, ScopedVars } from '@grafana/ui/src';
function luceneEscape(value) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
@ -220,7 +220,7 @@ export class TemplateSrv {
return values;
}
replace(target, scopedVars?, format?) {
replace(target: string, scopedVars?: ScopedVars, format?: string | Function) {
if (!target) {
return target;
}

View File

@ -8,7 +8,7 @@
<div class="gf-form" ng-show='ctrl.current.jsonData.authType == "credentials"'>
<label class="gf-form-label width-13">Credentials profile name</label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.database' placeholder="default"></input>
<input type="text" class="gf-form-input max-width-18 gf-form-input--has-help-icon" ng-model='ctrl.current.database' placeholder="default"></input>
<info-popover mode="right-absolute">
Credentials profile name, as specified in ~/.aws/credentials, leave blank for default
</info-popover>
@ -30,7 +30,7 @@
<div class="gf-form" ng-show='ctrl.current.jsonData.authType == "arn"'>
<label class="gf-form-label width-13">Assume Role ARN</label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.assumeRoleArn' placeholder="arn:aws:iam:*"></input>
<input type="text" class="gf-form-input max-width-18 gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.assumeRoleArn' placeholder="arn:aws:iam:*"></input>
<info-popover mode="right-absolute">
ARN of Assume Role
</info-popover>
@ -47,7 +47,7 @@
</div>
<div class="gf-form">
<label class="gf-form-label width-13">Custom Metrics</label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.customMetricsNamespaces' placeholder="Namespace1,Namespace2"></input>
<input type="text" class="gf-form-input max-width-18 gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.customMetricsNamespaces' placeholder="Namespace1,Namespace2"></input>
<info-popover mode="right-absolute">
Namespaces of Custom Metrics
</info-popover>

View File

@ -16,7 +16,6 @@
"logos": {
"small": "img/amazon-web-services.png",
"large": "img/amazon-web-services.png"
},
"version": "5.0.0"
}
}
}

View File

@ -36,7 +36,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Min time interval</span>
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
<input type="text" class="gf-form-input width-6 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
<info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
for example <code>1m</code> if your data is written every minute.

View File

@ -14,10 +14,7 @@
"small": "img/elasticsearch.svg",
"large": "img/elasticsearch.svg"
},
"links": [
{"name": "elastic.co", "url": "https://www.elastic.co/products/elasticsearch"}
],
"version": "5.0.0"
"links": [{ "name": "elastic.co", "url": "https://www.elastic.co/products/elasticsearch" }]
},
"alerting": true,

View File

@ -16,7 +16,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Subscription Id</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.subscriptionId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.subscriptionId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
<info-popover mode="right-absolute">
<p>In the Azure Portal, navigate to Subscriptions -> Choose subscription -> Overview -> Subscription ID.</p>
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
@ -27,7 +27,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Tenant Id</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.tenantId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.tenantId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
<info-popover mode="right-absolute">
<p>In the Azure Portal, navigate to Azure Active Directory -> Properties -> Directory ID.</p>
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal">**Click
@ -38,7 +38,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Client Id</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.clientId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.clientId" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
<info-popover mode="right-absolute">
<p>In the Azure Portal, navigate to Azure Active Directory -> App Registrations -> Choose your app ->
Application ID.</p>
@ -50,7 +50,7 @@
<div class="gf-form-inline" ng-if="!ctrl.current.secureJsonFields.clientSecret">
<div class="gf-form">
<span class="gf-form-label width-9">Client Secret</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.secureJsonData.clientSecret" placeholder=""></input>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.secureJsonData.clientSecret" placeholder=""></input>
<info-popover mode="right-absolute">
<p>To create a new key, log in to Azure Portal, navigate to Azure Active Directory -> App Registrations ->
Choose your
@ -83,7 +83,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Subscription Id</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.logAnalyticsSubscriptionId"
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.logAnalyticsSubscriptionId"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" />
<info-popover mode="right-absolute">
<p>In the Azure Portal, navigate to Subscriptions -> Choose subscription -> Overview -> Subscription ID.</p>
@ -95,7 +95,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Tenant Id</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.logAnalyticsTenantId"
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.logAnalyticsTenantId"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" />
<info-popover mode="right-absolute">
<p>In the Azure Portal, navigate to Azure Active Directory -> Properties -> Directory ID.</p>
@ -107,7 +107,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Client Id</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.logAnalyticsClientId"
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.logAnalyticsClientId"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"></input>
<info-popover mode="right-absolute">
<p>In the Azure Portal, navigate to Azure Active Directory -> App Registrations -> Choose your app ->
@ -121,7 +121,7 @@
<div class="gf-form-inline" ng-if="!ctrl.current.secureJsonFields.logAnalyticsClientSecret">
<div class="gf-form">
<span class="gf-form-label width-9">Client Secret</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.secureJsonData.logAnalyticsClientSecret"
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.secureJsonData.logAnalyticsClientSecret"
placeholder="" />
<info-popover mode="right-absolute">
<p>To create a new key, log in to Azure Portal, navigate to Azure Active Directory -> App Registrations ->
@ -168,7 +168,7 @@
<div class="gf-form-inline" ng-if="!ctrl.current.secureJsonFields.appInsightsApiKey">
<div class="gf-form">
<span class="gf-form-label width-9">API Key</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.secureJsonData.appInsightsApiKey"
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.secureJsonData.appInsightsApiKey"
placeholder="" />
<info-popover mode="right-absolute">
<p>Section 2 of the Quickstart guide shows where to find/create the API Key:</p>
@ -185,7 +185,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Application Id</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.jsonData.appInsightsAppId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></input>
<input class="gf-form-input width-30 gf-form-input--has-help-icon" type="text" ng-model="ctrl.current.jsonData.appInsightsAppId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></input>
<info-popover mode="right-absolute">
<p>Section 2 of the Quickstart guide shows where to find the Application ID:</p>
<a target="_blank" href="https://dev.applicationinsights.io/quickstart/">**Click here to open the Application

View File

@ -31,7 +31,6 @@
"name": "Graphite 1.1 Release",
"url": "https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/"
}
],
"version": "5.0.0"
]
}
}

View File

@ -41,7 +41,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">Min time interval</span>
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
<input type="text" class="gf-form-input width-6 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
<info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
for example <code>1m</code> if your data is written every minute.

View File

@ -22,7 +22,6 @@
"logos": {
"small": "img/influxdb_logo.svg",
"large": "img/influxdb_logo.svg"
},
"version": "5.0.0"
}
}
}

View File

@ -5,7 +5,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-8">Maximum lines</span>
<input type="text" class="gf-form-input width-8" ng-model="ctrl.current.jsonData.maxLines" spellcheck='false' placeholder="1000"></input>
<input type="text" class="gf-form-input width-8 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxLines" spellcheck='false' placeholder="1000"></input>
<info-popover mode="right-absolute">
Loki queries must contain a limit of the maximum number of lines returned (default: 1000).
Increase this limit to have a bigger result set for ad-hoc analysis.

View File

@ -1,4 +1,4 @@
import ansicolor from 'ansicolor';
import ansicolor from 'vendor/ansicolor/ansicolor';
import _ from 'lodash';
import moment from 'moment';

View File

@ -50,7 +50,7 @@
<div class="gf-form-group">
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max open</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input>
<info-popover mode="right-absolute">
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater than 0 and the
<i>Max open connections</i> is less than <i>Max idle connections</i>, then <i>Max idle connections</i> will be
@ -60,7 +60,7 @@
</div>
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max idle</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input>
<info-popover mode="right-absolute">
The maximum number of connections in the idle connection pool. If <i>Max open connections</i> is greater than 0 but
less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to match the
@ -69,7 +69,7 @@
</div>
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max lifetime</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input>
<info-popover mode="right-absolute">
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.
</info-popover>
@ -82,7 +82,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Min time interval</span>
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="1m"></input>
<input type="text" class="gf-form-input width-6 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="1m"></input>
<info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
for example <code>1m</code> if your data is written every minute.

View File

@ -44,7 +44,7 @@
<div class="gf-form-group">
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max open</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input>
<info-popover mode="right-absolute">
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater than 0 and the
<i>Max open connections</i> is less than <i>Max idle connections</i>, then <i>Max idle connections</i> will be
@ -54,7 +54,7 @@
</div>
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max idle</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input>
<info-popover mode="right-absolute">
The maximum number of connections in the idle connection pool. If <i>Max open connections</i> is greater than 0 but
less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to match the
@ -63,7 +63,7 @@
</div>
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max lifetime</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input>
<info-popover mode="right-absolute">
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.<br/><br/>
This should always be lower than configured <a href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_wait_timeout" target="_blank">wait_timeout</a> in MySQL.
@ -77,7 +77,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Min time interval</span>
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="1m"></input>
<input type="text" class="gf-form-input width-6 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="1m"></input>
<info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
for example <code>1m</code> if your data is written every minute.

View File

@ -12,8 +12,7 @@
"logos": {
"small": "img/mysql_logo.svg",
"large": "img/mysql_logo.svg"
},
"version": "5.0.0"
}
},
"alerting": true,

View File

@ -49,7 +49,7 @@
<div class="gf-form-inline">
<div class="gf-form max-width-25">
<label class="gf-form-label query-keyword width-8">Down sample</label>
<input type="text" class="gf-form-input"
<input type="text" class="gf-form-input gf-form-input--has-help-icon"
ng-model="ctrl.target.downsampleInterval"
ng-model-onblur
ng-change="ctrl.targetBlur()"
@ -139,7 +139,7 @@
</div>
<gf-form-switch class="gf-form"
label="Group by"
label="Group by"
label-class="query-keyword"
checked="ctrl.target.currentFilterGroupBy"
on-change="ctrl.targetBlur()">

View File

@ -18,7 +18,6 @@
"logos": {
"small": "img/opentsdb_logo.png",
"large": "img/opentsdb_logo.png"
},
"version": "5.0.0"
}
}
}

View File

@ -43,7 +43,7 @@
<div class="gf-form-group">
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max open</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxOpenConns" placeholder="unlimited"></input>
<info-popover mode="right-absolute">
The maximum number of open connections to the database. If <i>Max idle connections</i> is greater than 0 and the
<i>Max open connections</i> is less than <i>Max idle connections</i>, then <i>Max idle connections</i> will be
@ -53,7 +53,7 @@
</div>
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max idle</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxIdleConns" placeholder="2"></input>
<info-popover mode="right-absolute">
The maximum number of connections in the idle connection pool. If <i>Max open connections</i> is greater than 0 but
less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to match the
@ -62,7 +62,7 @@
</div>
<div class="gf-form max-width-15">
<span class="gf-form-label width-7">Max lifetime</span>
<input type="number" min="0" class="gf-form-input" ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input>
<input type="number" min="0" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.connMaxLifetime" placeholder="14400"></input>
<info-popover mode="right-absolute">
The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever.
</info-popover>
@ -95,7 +95,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Min time interval</span>
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="1m"></input>
<input type="text" class="gf-form-input width-6 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="1m"></input>
<info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
for example <code>1m</code> if your data is written every minute.

View File

@ -12,8 +12,7 @@
"logos": {
"small": "img/postgresql_logo.svg",
"large": "img/postgresql_logo.svg"
},
"version": "5.0.0"
}
},
"alerting": true,

View File

@ -5,7 +5,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-8">Scrape interval</span>
<input type="text" class="gf-form-input width-8" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="15s"></input>
<input type="text" class="gf-form-input width-8 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="15s"></input>
<info-popover mode="right-absolute">
Set this to your global scrape interval defined in your Prometheus config file. This will be used as a lower limit for the
Prometheus step query parameter.
@ -16,7 +16,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-8">Query timeout</span>
<input type="text" class="gf-form-input width-8" ng-model="ctrl.current.jsonData.queryTimeout" spellcheck='false' placeholder="60s"></input>
<input type="text" class="gf-form-input width-8 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.queryTimeout" spellcheck='false' placeholder="60s"></input>
<info-popover mode="right-absolute">
Set the Prometheus query timeout.
</info-popover>

View File

@ -10,7 +10,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label">Legend</label>
<input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat" spellcheck='false' placeholder="legend format"
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model="ctrl.target.legendFormat" spellcheck='false' placeholder="legend format"
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
</input>
<info-popover mode="right-absolute">
@ -21,7 +21,7 @@
<div class="gf-form">
<label class="gf-form-label width-6">Min step</label>
<input type="text" class="gf-form-input width-8" ng-model="ctrl.target.interval" data-placement="right" spellcheck='false'
<input type="text" class="gf-form-input width-8 gf-form-input--has-help-icon" ng-model="ctrl.target.interval" data-placement="right" spellcheck='false'
placeholder="{{ctrl.panelCtrl.interval}}" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.refreshMetricData()"
/>
<info-popover mode="right-absolute">

View File

@ -42,7 +42,6 @@
"name": "Prometheus",
"url": "https://prometheus.io/"
}
],
"version": "5.0.0"
]
}
}

View File

@ -14,7 +14,6 @@
"logos": {
"small": "img/icn-singlestat-panel.svg",
"large": "img/icn-singlestat-panel.svg"
},
"version": "5.0.0"
}
}
}

View File

@ -14,7 +14,6 @@
"logos": {
"small": "img/icn-dashlist-panel.svg",
"large": "img/icn-dashlist-panel.svg"
},
"version": "5.0.0"
}
}
}

View File

@ -14,7 +14,6 @@
"logos": {
"small": "img/icn-graph-panel.svg",
"large": "img/icn-graph-panel.svg"
},
"version": "5.0.0"
}
}
}

View File

@ -18,7 +18,6 @@
"links": [
{ "name": "Brendan Gregg - Heatmaps", "url": "http://www.brendangregg.com/heatmaps.html" },
{ "name": "Brendan Gregg - Latency Heatmaps", "url": " http://www.brendangregg.com/HeatMaps/latency.html" }
],
"version": "5.0.0"
]
}
}

View File

@ -14,7 +14,6 @@
"logos": {
"small": "img/icn-dashlist-panel.svg",
"large": "img/icn-dashlist-panel.svg"
},
"version": "5.0.0"
}
}
}

View File

@ -14,7 +14,6 @@
"logos": {
"small": "img/icn-singlestat-panel.svg",
"large": "img/icn-singlestat-panel.svg"
},
"version": "5.0.0"
}
}
}

View File

@ -14,7 +14,6 @@
"logos": {
"small": "img/icn-table-panel.svg",
"large": "img/icn-table-panel.svg"
},
"version": "5.0.0"
}
}
}

View File

@ -13,7 +13,6 @@
"logos": {
"small": "img/icn-text-panel.svg",
"large": "img/icn-text-panel.svg"
},
"version": "5.0.0"
}
}
}

View File

@ -81,6 +81,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
template: '<react-container />',
pageClass: 'dashboard-solo',
routeInfo: DashboardRouteInfo.Normal,
reloadOnSearch: false,
resolve: {
component: () => SoloPanelPage,
},
@ -89,6 +90,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
template: '<react-container />',
pageClass: 'dashboard-solo',
routeInfo: DashboardRouteInfo.Normal,
reloadOnSearch: false,
resolve: {
component: () => SoloPanelPage,
},

View File

@ -57,34 +57,34 @@ $orange: #eb7b18;
$purple: #9933cc;
$variable: #32d1df;
$brand-primary: $orange;
$brand-success: $green-base;
$brand-warning: $brand-primary;
$brand-danger: $red-base;
$brand-primary: #eb7b18;
$brand-success: #299c46;
$brand-warning: #eb7b18;
$brand-danger: #e02f44;
$query-red: $red-base;
$query-red: #e02f44;
$query-green: #74e680;
$query-purple: #fe85fc;
$query-orange: #eb7b18;
$query-keyword: #66d9ef;
$query-orange: $orange;
// Status colors
// -------------------------
$online: $green-base;
$online: #299c46;
$warn: #f79520;
$critical: $red-base;
$critical: #e02f44;
// Scaffolding
// -------------------------
$body-bg: #161719;
$page-bg: #161719;
$body-color: $gray-4;
$text-color: $gray-4;
$text-color-strong: $white;
$text-color-weak: $gray-2;
$text-color-faint: $dark-10;
$text-color-emphasis: $gray-5;
$body-color: #d8d9da;
$text-color: #d8d9da;
$text-color-strong: #ffffff;
$text-color-weak: #8e8e8e;
$text-color-faint: #222426;
$text-color-emphasis: #ececec;
$text-shadow-faint: 1px 1px 4px rgb(45, 45, 45);
$textShadow: none;
@ -102,14 +102,14 @@ $edit-gradient: linear-gradient(180deg, $dark-2 50%, $input-black);
// Links
// -------------------------
$link-color: darken($white, 11%);
$link-color-disabled: darken($link-color, 30%);
$link-hover-color: $white;
$external-link-color: $blue-light;
$link-color: #e3e3e3;
$link-color-disabled: #e3e3e3;
$link-hover-color: #ffffff;
$external-link-color: #33b5e5;
// Typography
// -------------------------
$headings-color: darken($white, 11%);
$headings-color: #e3e3e3;
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;

View File

@ -49,34 +49,34 @@ $orange: #ff7941;
$purple: #9954bb;
$variable: #0083b3;
$brand-primary: $orange;
$brand-success: $green-base;
$brand-warning: $orange;
$brand-danger: $red-base;
$brand-primary: #ff7941;
$brand-success: #3eb15b;
$brand-warning: #ff7941;
$brand-danger: #e02f44;
$query-red: $red-base;
$query-green: $green-base;
$query-purple: $purple;
$query-orange: $orange;
$query-keyword: $blue-base;
$query-red: #e02f44;
$query-green: #3eb15b;
$query-purple: #9954bb;
$query-orange: #ff7941;
$query-keyword: #3274d9;
// Status colors
// -------------------------
$online: $green-shade;
$online: #369b4f;
$warn: #f79520;
$critical: $red-shade;
$critical: #c4162a;
// Scaffolding
// -------------------------
$body-bg: #f7f8fa;
$page-bg: #f7f8fa;
$body-color: $gray-1;
$text-color: $gray-1;
$text-color-strong: $dark-1;
$text-color-weak: $gray-2;
$text-color-faint: $gray-4;
$text-color-emphasis: $dark-2;
$body-color: #52545c;
$text-color: #52545c;
$text-color-strong: #41444b;
$text-color-weak: #767980;
$text-color-faint: #35373f;
$text-color-emphasis: #dde4ed;
$text-shadow-faint: none;
@ -88,14 +88,14 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
// Links
// -------------------------
$link-color: $gray-1;
$link-color-disabled: lighten($link-color, 30%);
$link-hover-color: darken($link-color, 20%);
$external-link-color: $blue-shade;
$link-color: #52545c;
$link-color-disabled: #9ea0a9;
$link-hover-color: #222326;
$external-link-color: #5794f2;
// Typography
// -------------------------
$headings-color: $text-color;
$headings-color: #52545c;
$abbr-border-color: $gray-2 !default;
$text-muted: $text-color-weak;

View File

@ -93,7 +93,9 @@
}
.gicon {
opacity: 0.9;
opacity: 0.7;
width: 14px;
height: 14px;
}
}
}

View File

@ -250,6 +250,10 @@ $input-border: 1px solid $input-border-color;
&--plaintext {
white-space: unset;
}
&--has-help-icon {
padding-right: $input-padding-x * 3;
}
}
.gf-form-hint {

View File

@ -15,6 +15,10 @@
height: 100%;
width: 5rem;
box-sizing: border-box;
&.gf-form-input--has-help-icon {
padding-right: $input-padding-x * 3;
}
}
.tag {

471
public/vendor/ansicolor/ansicolor.ts vendored Normal file
View File

@ -0,0 +1,471 @@
// Vendored and converted to TS, source: https://github.com/xpl/ansicolor/blob/b82360563ed29de444dc7618b9236191e0a77096/ansicolor.js
// License: Unlicense, author: https://github.com/xpl
const O = Object;
/* See https://misc.flogisoft.com/bash/tip_colors_and_formatting
------------------------------------------------------------------------ */
const colorCodes = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'lightGray', '', 'default'],
colorCodesLight = [
'darkGray',
'lightRed',
'lightGreen',
'lightYellow',
'lightBlue',
'lightMagenta',
'lightCyan',
'white',
'',
],
styleCodes = ['', 'bright', 'dim', 'italic', 'underline', '', '', 'inverse'],
asBright = {
red: 'lightRed',
green: 'lightGreen',
yellow: 'lightYellow',
blue: 'lightBlue',
magenta: 'lightMagenta',
cyan: 'lightCyan',
black: 'darkGray',
lightGray: 'white',
},
types = {
0: 'style',
2: 'unstyle',
3: 'color',
9: 'colorLight',
4: 'bgColor',
10: 'bgColorLight',
},
subtypes = {
color: colorCodes,
colorLight: colorCodesLight,
bgColor: colorCodes,
bgColorLight: colorCodesLight,
style: styleCodes,
unstyle: styleCodes,
};
/* ------------------------------------------------------------------------ */
const clean = obj => {
for (const k in obj) {
if (!obj[k]) {
delete obj[k];
}
}
return O.keys(obj).length === 0 ? undefined : obj;
};
/* ------------------------------------------------------------------------ */
class Color {
background: string;
name: string;
brightness: number;
constructor(background?, name?, brightness?) {
this.background = background;
this.name = name;
this.brightness = brightness;
}
get inverse() {
return new Color(!this.background, this.name || (this.background ? 'black' : 'white'), this.brightness);
}
get clean() {
return clean({
name: this.name === 'default' ? '' : this.name,
bright: this.brightness === Code.bright,
dim: this.brightness === Code.dim,
});
}
defaultBrightness(value) {
return new Color(this.background, this.name, this.brightness || value);
}
css(inverted) {
const color = inverted ? this.inverse : this;
const rgbName = (color.brightness === Code.bright && asBright[color.name]) || color.name;
const prop = color.background ? 'background:' : 'color:',
rgb = Colors.rgb[rgbName],
alpha = this.brightness === Code.dim ? 0.5 : 1;
return rgb
? prop + 'rgba(' + [...rgb, alpha].join(',') + ');'
: !color.background && alpha < 1 ? 'color:rgba(0,0,0,0.5);' : ''; // Chrome does not support 'opacity' property...
}
}
/* ------------------------------------------------------------------------ */
class Code {
static reset = 0;
static bright = 1;
static dim = 2;
static inverse = 7;
static noBrightness = 22;
static noItalic = 23;
static noUnderline = 24;
static noInverse = 27;
static noColor = 39;
static noBgColor = 49;
value: number;
constructor(n?) {
if (n !== undefined) {
this.value = Number(n);
}
}
get type() {
return types[Math.floor(this.value / 10)];
}
get subtype() {
return subtypes[this.type][this.value % 10];
}
get str() {
return this.value ? '\u001b[' + this.value + 'm' : '';
}
static str(x) {
return new Code(x).str;
}
get isBrightness() {
return this.value === Code.noBrightness || this.value === Code.bright || this.value === Code.dim;
}
}
/* ------------------------------------------------------------------------ */
const replaceAll = (str, a, b) => str.split(a).join(b);
/* ANSI brightness codes do not overlap, e.g. "{bright}{dim}foo" will be rendered bright (not dim).
So we fix it by adding brightness canceling before each brightness code, so the former example gets
converted to "{noBrightness}{bright}{noBrightness}{dim}foo" this way it gets rendered as expected.
*/
const denormalizeBrightness = s => s.replace(/(\u001b\[(1|2)m)/g, '\u001b[22m$1');
const normalizeBrightness = s => s.replace(/\u001b\[22m(\u001b\[(1|2)m)/g, '$1');
const wrap = (x, openCode, closeCode) => {
const open = Code.str(openCode),
close = Code.str(closeCode);
return String(x)
.split('\n')
.map(line => denormalizeBrightness(open + replaceAll(normalizeBrightness(line), close, open) + close))
.join('\n');
};
/* ------------------------------------------------------------------------ */
const camel = (a, b) => a + b.charAt(0).toUpperCase() + b.slice(1);
const stringWrappingMethods = (() =>
[
...colorCodes.map(
(k, i) =>
!k
? []
: [
// color methods
[k, 30 + i, Code.noColor],
[camel('bg', k), 40 + i, Code.noBgColor],
]
),
...colorCodesLight.map(
(k, i) =>
!k
? []
: [
// light color methods
[k, 90 + i, Code.noColor],
[camel('bg', k), 100 + i, Code.noBgColor],
]
),
/* THIS ONE IS FOR BACKWARDS COMPATIBILITY WITH PREVIOUS VERSIONS (had 'bright' instead of 'light' for backgrounds)
*/
...['', 'BrightRed', 'BrightGreen', 'BrightYellow', 'BrightBlue', 'BrightMagenta', 'BrightCyan'].map(
(k, i) => (!k ? [] : [['bg' + k, 100 + i, Code.noBgColor]])
),
...styleCodes.map(
(k, i) =>
!k
? []
: [
// style methods
[k, i, k === 'bright' || k === 'dim' ? Code.noBrightness : 20 + i],
]
),
].reduce((a, b) => a.concat(b)))();
/* ------------------------------------------------------------------------ */
const assignStringWrappingAPI = (target, wrapBefore = target) =>
stringWrappingMethods.reduce(
(memo, [k, open, close]) =>
O.defineProperty(memo, k, {
get: () => assignStringWrappingAPI(str => wrapBefore(wrap(str, open, close))),
}),
target
);
/* ------------------------------------------------------------------------ */
const TEXT = 0,
BRACKET = 1,
CODE = 2;
function rawParse(s) {
let state = TEXT,
buffer = '',
text = '',
code = '',
codes = [];
const spans = [];
for (let i = 0, n = s.length; i < n; i++) {
const c = s[i];
buffer += c;
switch (state) {
case TEXT: {
if (c === '\u001b') {
state = BRACKET;
buffer = c;
} else {
text += c;
}
break;
}
case BRACKET:
if (c === '[') {
state = CODE;
code = '';
codes = [];
} else {
state = TEXT;
text += buffer;
}
break;
case CODE:
if (c >= '0' && c <= '9') {
code += c;
} else if (c === ';') {
codes.push(new Code(code));
code = '';
} else if (c === 'm' && code.length) {
codes.push(new Code(code));
for (const code of codes) {
spans.push({ text, code });
text = '';
}
state = TEXT;
} else {
state = TEXT;
text += buffer;
}
}
}
if (state !== TEXT) {
text += buffer;
}
if (text) {
spans.push({ text, code: new Code() });
}
return spans;
}
/* ------------------------------------------------------------------------ */
/**
* Represents an ANSI-escaped string.
*/
export default class Colors {
spans: any[];
static names = stringWrappingMethods.map(([k]) => k);
static rgb = {
black: [0, 0, 0],
darkGray: [100, 100, 100],
lightGray: [200, 200, 200],
white: [255, 255, 255],
red: [204, 0, 0],
lightRed: [255, 51, 0],
green: [0, 204, 0],
lightGreen: [51, 204, 51],
yellow: [204, 102, 0],
lightYellow: [255, 153, 51],
blue: [0, 0, 255],
lightBlue: [26, 140, 255],
magenta: [204, 0, 204],
lightMagenta: [255, 0, 255],
cyan: [0, 153, 255],
lightCyan: [0, 204, 255],
};
/**
* @param {string} s a string containing ANSI escape codes.
*/
constructor(s?) {
this.spans = s ? rawParse(s) : [];
}
get str() {
return this.spans.reduce((str, p) => str + p.text + p.code.str, '');
}
get parsed() {
let color, bgColor, brightness, styles;
function reset() {
(color = new Color()),
(bgColor = new Color(true /* background */)),
(brightness = undefined),
(styles = new Set());
}
reset();
return O.assign(new Colors(), {
spans: this.spans
.map(span => {
const c = span.code;
const inverted = styles.has('inverse'),
underline = styles.has('underline') ? 'text-decoration: underline;' : '',
italic = styles.has('italic') ? 'font-style: italic;' : '',
bold = brightness === Code.bright ? 'font-weight: bold;' : '';
const foreColor = color.defaultBrightness(brightness);
const styledSpan = O.assign(
{ css: bold + italic + underline + foreColor.css(inverted) + bgColor.css(inverted) },
clean({ bold: !!bold, color: foreColor.clean, bgColor: bgColor.clean }),
span
);
for (const k of styles) {
styledSpan[k] = true;
}
if (c.isBrightness) {
brightness = c.value;
} else if (span.code.value !== undefined) {
if (span.code.value === Code.reset) {
reset();
} else {
switch (span.code.type) {
case 'color':
case 'colorLight':
color = new Color(false, c.subtype);
break;
case 'bgColor':
case 'bgColorLight':
bgColor = new Color(true, c.subtype);
break;
case 'style':
styles.add(c.subtype);
break;
case 'unstyle':
styles.delete(c.subtype);
break;
}
}
}
return styledSpan;
})
.filter(s => s.text.length > 0),
});
}
/* Outputs with Chrome DevTools-compatible format */
get asChromeConsoleLogArguments() {
const spans = this.parsed.spans;
return [spans.map(s => '%c' + s.text).join(''), ...spans.map(s => s.css)];
}
get browserConsoleArguments() /* LEGACY, DEPRECATED */ {
return this.asChromeConsoleLogArguments;
}
/**
* @desc installs String prototype extensions
* @example
* require ('ansicolor').nice
* console.log ('foo'.bright.red)
*/
static get nice() {
Colors.names.forEach(k => {
if (!(k in String.prototype)) {
O.defineProperty(String.prototype, k, {
get: function() {
return Colors[k](this);
},
});
}
});
return Colors;
}
/**
* @desc parses a string containing ANSI escape codes
* @return {Colors} parsed representation.
*/
static parse(s) {
return new Colors(s).parsed;
}
/**
* @desc strips ANSI codes from a string
* @param {string} s a string containing ANSI escape codes.
* @return {string} clean string.
*/
static strip(s) {
return s.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]/g, ''); // hope V8 caches the regexp
}
/**
* @example
* const spans = [...ansi.parse ('\u001b[7m\u001b[7mfoo\u001b[7mbar\u001b[27m')]
*/
[Symbol.iterator]() {
return this.spans[Symbol.iterator]();
}
}
/* ------------------------------------------------------------------------ */
assignStringWrappingAPI(Colors, str => str);

View File

@ -1,33 +1,73 @@
import program from 'commander';
import chalk from 'chalk';
import { execTask } from './utils/execTask';
import chalk from 'chalk';
import { startTask } from './tasks/core.start';
import { buildTask } from './tasks/grafanaui.build';
import { releaseTask } from './tasks/grafanaui.release';
import { changelogTask } from './tasks/changelog';
import { cherryPickTask } from './tasks/cherrypick';
export type Task<T> = (options: T) => Promise<void>;
program.option('-d, --depreciate <scripts>', 'Inform about npm script deprecation', v => v.split(','));
// TODO: Refactor to commander commands
// This will enable us to have command scoped options and limit the ifs below
program
.option('-h, --hot', 'Runs front-end with hot reload enabled')
.option('-t, --theme', 'Watches for theme changes and regenerates variables.scss files')
.option('-d, --depreciate <scripts>', 'Inform about npm script deprecation', v => v.split(','))
.option('-b, --build', 'Created @grafana/ui build')
.option('-r, --release', 'Releases @grafana/ui to npm')
.parse(process.argv);
if (program.build) {
execTask('grafanaui.build');
} else if (program.release) {
execTask('grafanaui.release');
} else {
if (program.depreciate && program.depreciate.length === 2) {
console.log(
chalk.yellow.bold(
`[NPM script depreciation] ${program.depreciate[0]} is deprecated! Use ${program.depreciate[1]} instead!`
)
);
}
execTask('core.start', {
watchThemes: !!program.theme,
hot: !!program.hot,
.command('core:start')
.option('-h, --hot', 'Run front-end with HRM enabled')
.option('-t, --watchTheme', 'Watch for theme changes and regenerate variables.scss files')
.description('Starts Grafana front-end in development mode with watch enabled')
.action(async cmd => {
await execTask(startTask)({
watchThemes: cmd.theme,
hot: cmd.hot,
});
});
program
.command('gui:build')
.description('Builds @grafana/ui package to packages/grafana-ui/dist')
.action(async cmd => {
await execTask(buildTask)();
});
program
.command('gui:release')
.description('Prepares @grafana/ui release (and publishes to npm on demand)')
.option('-p, --publish', 'Publish @grafana/ui to npm registry')
.option('-u, --usePackageJsonVersion', 'Use version specified in package.json')
.action(async cmd => {
await execTask(releaseTask)({
publishToNpm: !!cmd.publish,
usePackageJsonVersion: !!cmd.usePackageJsonVersion,
});
});
program
.command('changelog')
.option('-m, --milestone <milestone>', 'Specify milestone')
.description('Builds changelog markdown')
.action(async cmd => {
if (!cmd.milestone) {
console.log('Please specify milestone, example: --m 6.0.1');
return;
}
await execTask(changelogTask)({
milestone: cmd.milestone,
});
});
program
.command('cherrypick')
.description('Helps find commits to cherry pick')
.action(async cmd => {
await execTask(cherryPickTask)({});
});
program.parse(process.argv);
if (program.depreciate && program.depreciate.length === 2) {
console.log(
chalk.yellow.bold(
`[NPM script depreciation] ${program.depreciate[0]} is deprecated! Use ${program.depreciate[1]} instead!`
)
);
}

View File

@ -0,0 +1,49 @@
import { Task, TaskRunner } from './task';
import axios from 'axios';
const githubGrafanaUrl = 'https://github.com/grafana/grafana';
interface ChangelogOptions {
milestone: string;
}
const changelogTaskRunner: TaskRunner<ChangelogOptions> = async ({ milestone }) => {
let client = axios.create({
baseURL: 'https://api.github.com/repos/grafana/grafana',
timeout: 10000,
});
const res = await client.get('/issues', {
params: {
state: 'closed',
labels: 'add to changelog',
},
});
let markdown = '';
for (const item of res.data) {
if (!item.milestone) {
console.log('Item missing milestone', item.number);
continue;
}
// For some reason I could not get the github api to filter on milestone and label
// So doing this filter here
if (item.milestone.title !== milestone) {
continue;
}
markdown += '* ' + item.title + '.';
markdown += ` [#${item.number}](${githubGrafanaUrl}/pull/${item.number})`;
markdown += `, [@${item.user.login}](${item.user.html_url})`;
markdown += '\n';
}
console.log(markdown);
};
export const changelogTask = new Task<ChangelogOptions>();
changelogTask.setName('Changelog generator task');
changelogTask.setRunner(changelogTaskRunner);

View File

@ -0,0 +1,42 @@
import { Task, TaskRunner } from './task';
import axios from 'axios';
interface CherryPickOptions {}
const cherryPickRunner: TaskRunner<CherryPickOptions> = async () => {
let client = axios.create({
baseURL: 'https://api.github.com/repos/grafana/grafana',
timeout: 10000,
});
const res = await client.get('/issues', {
params: {
state: 'closed',
labels: 'cherry-pick needed',
},
});
// sort by closed date
res.data.sort(function(a, b) {
return new Date(b.closed_at).getTime() - new Date(a.closed_at).getTime();
});
for (const item of res.data) {
if (!item.milestone) {
console.log(item.number + ' missing milestone!');
continue;
}
console.log(item.number + ' closed_at ' + item.closed_at + ' ' + item.html_url);
const issueDetails = await client.get(item.pull_request.url);
const commits = await client.get(issueDetails.data.commits_url);
for (const commit of commits.data) {
console.log(commit.commit.message + ' sha: ' + commit.sha);
}
}
};
export const cherryPickTask = new Task<CherryPickOptions>();
cherryPickTask.setName('Cherry pick task');
cherryPickTask.setRunner(cherryPickRunner);

View File

@ -1,35 +1,30 @@
import concurrently from 'concurrently';
import { Task } from '..';
import { Task, TaskRunner } from './task';
interface StartTaskOptions {
watchThemes: boolean;
hot: boolean;
}
const startTask: Task<StartTaskOptions> = async ({ watchThemes, hot }) => {
const jobs = [];
if (watchThemes) {
jobs.push({
const startTaskRunner: TaskRunner<StartTaskOptions> = async ({ watchThemes, hot }) => {
const jobs = [
watchThemes && {
command: 'nodemon -e ts -w ./packages/grafana-ui/src/themes -x yarn run themes:generate',
name: 'SASS variables generator',
});
}
if (!hot) {
jobs.push({
command: 'webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js',
name: 'Webpack',
});
} else {
jobs.push({
command: 'webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js',
name: 'Dev server',
});
}
},
hot
? {
command: 'webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js',
name: 'Dev server',
}
: {
command: 'webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js',
name: 'Webpack',
},
];
try {
await concurrently(jobs, {
await concurrently(jobs.filter(job => !!job), {
killOthers: ['failure', 'failure'],
});
} catch (e) {
@ -38,4 +33,6 @@ const startTask: Task<StartTaskOptions> = async ({ watchThemes, hot }) => {
}
};
export default startTask;
export const startTask = new Task<StartTaskOptions>();
startTask.setName('Core startTask');
startTask.setRunner(startTaskRunner);

View File

@ -1,95 +1,66 @@
import execa from 'execa';
import fs from 'fs';
import { Task } from '..';
import { changeCwdToGrafanaUi, restoreCwd } from '../utils/cwd';
import chalk from 'chalk';
import { startSpinner } from '../utils/startSpinner';
import { useSpinner } from '../utils/useSpinner';
import { Task, TaskRunner } from './task';
let distDir, cwd;
const clean = async () => {
const spinner = startSpinner('Cleaning');
try {
await execa('npm', ['run', 'clean']);
spinner.succeed();
} catch (e) {
spinner.fail();
throw e;
}
};
const clean = useSpinner<void>('Cleaning', async () => await execa('npm', ['run', 'clean']));
const compile = async () => {
const spinner = startSpinner('Compiling sources');
try {
await execa('tsc', ['-p', './tsconfig.build.json']);
spinner.succeed();
} catch (e) {
console.log(e);
spinner.fail();
}
};
const compile = useSpinner<void>('Compiling sources', () => execa('tsc', ['-p', './tsconfig.build.json']));
const rollup = async () => {
const spinner = startSpinner('Bundling');
try {
await execa('npm', ['run', 'build']);
spinner.succeed();
} catch (e) {
spinner.fail();
}
};
export const savePackage = async (path, pkg) => {
const spinner = startSpinner('Updating package.json');
const rollup = useSpinner<void>('Bundling', () => execa('npm', ['run', 'build']));
export const savePackage = useSpinner<{
path: string;
pkg: {};
}>('Updating package.json', async ({ path, pkg }) => {
return new Promise((resolve, reject) => {
fs.writeFile(path, JSON.stringify(pkg, null, 2), err => {
if (err) {
spinner.fail();
console.error(err);
reject(err);
return;
}
spinner.succeed();
resolve();
});
});
};
});
const preparePackage = async pkg => {
pkg.main = 'index.js';
pkg.types = 'index.d.ts';
await savePackage(`${cwd}/dist/package.json`, pkg);
};
const moveFiles = async () => {
const files = ['README.md', 'CHANGELOG.md', 'index.js'];
const spinner = startSpinner(`Moving ${files.join(', ')} files`);
const promises = files.map(file => {
return fs.copyFile(`${cwd}/${file}`, `${distDir}/${file}`, err => {
if (err) {
console.error(err);
return;
}
});
await savePackage({
path: `${cwd}/dist/package.json`,
pkg,
});
try {
await Promise.all(promises);
spinner.succeed();
} catch (e) {
spinner.fail();
}
};
const buildTask: Task<void> = async () => {
const moveFiles = () => {
const files = ['README.md', 'CHANGELOG.md', 'index.js'];
return useSpinner<void>(`Moving ${files.join(', ')} files`, async () => {
const promises = files.map(file => {
return new Promise((resolve, reject) => {
fs.copyFile(`${cwd}/${file}`, `${distDir}/${file}`, err => {
if (err) {
reject(err);
return;
}
resolve();
});
});
});
await Promise.all(promises);
})();
};
const buildTaskRunner: TaskRunner<void> = async () => {
cwd = changeCwdToGrafanaUi();
distDir = `${cwd}/dist`;
const pkg = require(`${cwd}/package.json`);
console.log(chalk.yellow(`Building ${pkg.name} @ ${pkg.version}`));
console.log(chalk.yellow(`Building ${pkg.name} (package.json version: ${pkg.version})`));
await clean();
await compile();
@ -100,4 +71,6 @@ const buildTask: Task<void> = async () => {
restoreCwd();
};
export default buildTask;
export const buildTask = new Task<void>();
buildTask.setName('@grafana/ui build');
buildTask.setRunner(buildTaskRunner);

View File

@ -1,14 +1,19 @@
import execa from 'execa';
import { Task } from '..';
import { execTask } from '../utils/execTask';
import { changeCwdToGrafanaUiDist, changeCwdToGrafanaUi } from '../utils/cwd';
import semver from 'semver';
import inquirer from 'inquirer';
import chalk from 'chalk';
import { startSpinner } from '../utils/startSpinner';
import { savePackage } from './grafanaui.build';
import { useSpinner } from '../utils/useSpinner';
import { savePackage, buildTask } from './grafanaui.build';
import { TaskRunner, Task } from './task';
type VersionBumpType = 'patch' | 'minor' | 'major';
type VersionBumpType = 'prerelease' | 'patch' | 'minor' | 'major';
interface ReleaseTaskOptions {
publishToNpm: boolean;
usePackageJsonVersion: boolean;
}
const promptBumpType = async () => {
return inquirer.prompt<{ type: VersionBumpType }>([
@ -16,7 +21,7 @@ const promptBumpType = async () => {
type: 'list',
message: 'Select version bump',
name: 'type',
choices: ['patch', 'minor', 'major'],
choices: ['prerelease', 'patch', 'minor', 'major'],
validate: answer => {
if (answer.length < 1) {
return 'You must choose something';
@ -28,13 +33,13 @@ const promptBumpType = async () => {
]);
};
const promptPrereleaseId = async () => {
const promptPrereleaseId = async (message = 'Is this a prerelease?', allowNo = true) => {
return inquirer.prompt<{ id: string }>([
{
type: 'list',
message: 'Is this a prerelease?',
message: message,
name: 'id',
choices: ['no', 'alpha', 'beta'],
choices: allowNo ? ['no', 'alpha', 'beta'] : ['alpha', 'beta'],
validate: answer => {
if (answer.length < 1) {
return 'You must choose something';
@ -57,47 +62,45 @@ const promptConfirm = async (message?: string) => {
]);
};
const bumpVersion = async (version: string) => {
const spinner = startSpinner(`Saving version ${version} to package.json`);
changeCwdToGrafanaUi();
try {
const bumpVersion = (version: string) =>
useSpinner<void>(`Saving version ${version} to package.json`, async () => {
changeCwdToGrafanaUi();
await execa('npm', ['version', version]);
spinner.succeed();
} catch (e) {
console.log(e);
spinner.fail();
}
changeCwdToGrafanaUiDist();
const pkg = require(`${process.cwd()}/package.json`);
pkg.version = version;
await savePackage({ path: `${process.cwd()}/package.json`, pkg });
})();
changeCwdToGrafanaUiDist();
const pkg = require(`${process.cwd()}/package.json`);
pkg.version = version;
await savePackage(`${process.cwd()}/package.json`, pkg);
};
const publishPackage = (name: string, version: string) =>
useSpinner<void>(`Publishing ${name} @ ${version} to npm registry...`, async () => {
changeCwdToGrafanaUiDist();
console.log(chalk.yellowBright.bold(`\nReview dist package.json before proceeding!\n`));
const { confirmed } = await promptConfirm('Are you ready to publish to npm?');
const publishPackage = async (name: string, version: string) => {
changeCwdToGrafanaUiDist();
console.log(chalk.yellowBright.bold(`\nReview dist package.json before proceeding!\n`));
const { confirmed } = await promptConfirm('Are you ready to publish to npm?');
if (!confirmed) {
process.exit();
}
const spinner = startSpinner(`Publishing ${name} @ ${version} to npm registry...`);
try {
if (!confirmed) {
process.exit();
}
await execa('npm', ['publish', '--access', 'public']);
spinner.succeed();
} catch (e) {
console.log(e);
spinner.fail();
})();
const ensureMasterBranch = async () => {
const currentBranch = await execa.stdout('git', ['symbolic-ref', '--short', 'HEAD']);
const status = await execa.stdout('git', ['status', '--porcelain']);
if (currentBranch !== 'master' && status !== '') {
console.error(chalk.red.bold('You need to be on clean master branch to release @grafana/ui'));
process.exit(1);
}
};
const releaseTask: Task<void> = async () => {
await execTask('grafanaui.build');
const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({ publishToNpm, usePackageJsonVersion }) => {
if (publishToNpm) {
await ensureMasterBranch();
}
await execTask(buildTask)();
let releaseConfirmed = false;
let nextVersion;
changeCwdToGrafanaUiDist();
@ -107,27 +110,56 @@ const releaseTask: Task<void> = async () => {
console.log(`Current version: ${pkg.version}`);
do {
const { type } = await promptBumpType();
const { id } = await promptPrereleaseId();
if (id !== 'no') {
nextVersion = semver.inc(pkg.version, `pre${type}`, id);
if (!usePackageJsonVersion) {
const { type } = await promptBumpType();
console.log(type);
if (type === 'prerelease') {
const { id } = await promptPrereleaseId('What kind of prerelease?', false);
nextVersion = semver.inc(pkg.version, type, id);
} else {
const { id } = await promptPrereleaseId();
if (id !== 'no') {
nextVersion = semver.inc(pkg.version, `pre${type}`, id);
} else {
nextVersion = semver.inc(pkg.version, type);
}
}
} else {
nextVersion = semver.inc(pkg.version, type);
nextVersion = pkg.version;
}
console.log(chalk.yellowBright.bold(`You are going to release a new version of ${pkg.name}`));
console.log(chalk.green(`Version bump: ${pkg.version} ->`), chalk.bold.yellowBright(`${nextVersion}`));
if (usePackageJsonVersion) {
console.log(chalk.green(`Version based on package.json: `), chalk.bold.yellowBright(`${nextVersion}`));
} else {
console.log(chalk.green(`Version bump: ${pkg.version} ->`), chalk.bold.yellowBright(`${nextVersion}`));
}
const { confirmed } = await promptConfirm();
releaseConfirmed = confirmed;
} while (!releaseConfirmed);
await bumpVersion(nextVersion);
await publishPackage(pkg.name, nextVersion);
if (!usePackageJsonVersion) {
await bumpVersion(nextVersion);
}
console.log(chalk.green(`\nVersion ${nextVersion} of ${pkg.name} succesfully released!`));
console.log(chalk.yellow(`\nUpdated @grafana/ui/package.json with version bump created - COMMIT THIS FILE!`));
if (publishToNpm) {
await publishPackage(pkg.name, nextVersion);
console.log(chalk.green(`\nVersion ${nextVersion} of ${pkg.name} succesfully released!`));
console.log(chalk.yellow(`\nUpdated @grafana/ui/package.json with version bump created - COMMIT THIS FILE!`));
process.exit();
} else {
console.log(
chalk.green(
`\nVersion ${nextVersion} of ${pkg.name} succesfully prepared for release. See packages/grafana-ui/dist`
)
);
console.log(chalk.green(`\nTo publish to npm registry run`), chalk.bold.blue(`npm run gui:publish`));
}
};
export default releaseTask;
export const releaseTask = new Task<ReleaseTaskOptions>();
releaseTask.setName('@grafana/ui release');
releaseTask.setRunner(releaseTaskRunner);

23
scripts/cli/tasks/task.ts Normal file
View File

@ -0,0 +1,23 @@
export type TaskRunner<T> = (options: T) => Promise<void>;
export class Task<TOptions> {
name: string;
runner: (options: TOptions) => Promise<void>;
options: TOptions;
setName = name => {
this.name = name;
};
setRunner = (runner: TaskRunner<TOptions>) => {
this.runner = runner;
};
setOptions = options => {
this.options = options;
};
exec = () => {
return this.runner(this.options);
};
}

View File

@ -1,6 +1,15 @@
import { Task } from '..';
import { Task } from '../tasks/task';
import chalk from 'chalk';
export const execTask = async <T>(taskName, options?: T) => {
const task = await import(`${__dirname}/../tasks/${taskName}.ts`);
return task.default(options) as Task<T>;
export const execTask = <TOptions>(task: Task<TOptions>) => async (options: TOptions) => {
console.log(chalk.yellow(`Running ${chalk.bold(task.name)} task`));
task.setOptions(options);
try {
console.group();
await task.exec();
console.groupEnd();
} catch (e) {
console.log(e);
process.exit(1);
}
};

View File

@ -1,7 +0,0 @@
import ora from 'ora';
export const startSpinner = (label: string) => {
const spinner = new ora(label);
spinner.start();
return spinner;
};

View File

@ -0,0 +1,20 @@
import ora from 'ora';
type FnToSpin<T> = (options: T) => Promise<void>;
export const useSpinner = <T>(spinnerLabel: string, fn: FnToSpin<T>, killProcess = true) => {
return async (options: T) => {
const spinner = new ora(spinnerLabel);
spinner.start();
try {
await fn(options);
spinner.succeed();
} catch (e) {
spinner.fail();
console.log(e);
if (killProcess) {
process.exit(1);
}
}
};
};

View File

@ -32,5 +32,5 @@
"sass": ["sass"]
}
},
"include": ["public/app/**/*.ts", "public/app/**/*.tsx", "public/test/**/*.ts"]
"include": ["public/app/**/*.ts", "public/app/**/*.tsx", "public/test/**/*.ts", "public/vendor/**/*.ts"]
}

191
vendor/github.com/go-ini/ini/LICENSE generated vendored
View File

@ -1,191 +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:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
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
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 2014 Unknwon
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.

View File

@ -1,32 +0,0 @@
// Copyright 2016 Unknwon
//
// 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.
package ini
import (
"fmt"
)
type ErrDelimiterNotFound struct {
Line string
}
func IsErrDelimiterNotFound(err error) bool {
_, ok := err.(ErrDelimiterNotFound)
return ok
}
func (err ErrDelimiterNotFound) Error() string {
return fmt.Sprintf("key-value delimiter not found: %s", err.Line)
}

407
vendor/github.com/go-ini/ini/file.go generated vendored
View File

@ -1,407 +0,0 @@
// Copyright 2017 Unknwon
//
// 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.
package ini
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"sync"
)
// File represents a combination of a or more INI file(s) in memory.
type File struct {
options LoadOptions
dataSources []dataSource
// Should make things safe, but sometimes doesn't matter.
BlockMode bool
lock sync.RWMutex
// To keep data in order.
sectionList []string
// Actual data is stored here.
sections map[string]*Section
NameMapper
ValueMapper
}
// newFile initializes File object with given data sources.
func newFile(dataSources []dataSource, opts LoadOptions) *File {
return &File{
BlockMode: true,
dataSources: dataSources,
sections: make(map[string]*Section),
sectionList: make([]string, 0, 10),
options: opts,
}
}
// Empty returns an empty file object.
func Empty() *File {
// Ignore error here, we sure our data is good.
f, _ := Load([]byte(""))
return f
}
// NewSection creates a new section.
func (f *File) NewSection(name string) (*Section, error) {
if len(name) == 0 {
return nil, errors.New("error creating new section: empty section name")
} else if f.options.Insensitive && name != DEFAULT_SECTION {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
if inSlice(name, f.sectionList) {
return f.sections[name], nil
}
f.sectionList = append(f.sectionList, name)
f.sections[name] = newSection(f, name)
return f.sections[name], nil
}
// NewRawSection creates a new section with an unparseable body.
func (f *File) NewRawSection(name, body string) (*Section, error) {
section, err := f.NewSection(name)
if err != nil {
return nil, err
}
section.isRawSection = true
section.rawBody = body
return section, nil
}
// NewSections creates a list of sections.
func (f *File) NewSections(names ...string) (err error) {
for _, name := range names {
if _, err = f.NewSection(name); err != nil {
return err
}
}
return nil
}
// GetSection returns section by given name.
func (f *File) GetSection(name string) (*Section, error) {
if len(name) == 0 {
name = DEFAULT_SECTION
}
if f.options.Insensitive {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
sec := f.sections[name]
if sec == nil {
return nil, fmt.Errorf("section '%s' does not exist", name)
}
return sec, nil
}
// Section assumes named section exists and returns a zero-value when not.
func (f *File) Section(name string) *Section {
sec, err := f.GetSection(name)
if err != nil {
// Note: It's OK here because the only possible error is empty section name,
// but if it's empty, this piece of code won't be executed.
sec, _ = f.NewSection(name)
return sec
}
return sec
}
// Section returns list of Section.
func (f *File) Sections() []*Section {
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
sections := make([]*Section, len(f.sectionList))
for i, name := range f.sectionList {
sections[i] = f.sections[name]
}
return sections
}
// ChildSections returns a list of child sections of given section name.
func (f *File) ChildSections(name string) []*Section {
return f.Section(name).ChildSections()
}
// SectionStrings returns list of section names.
func (f *File) SectionStrings() []string {
list := make([]string, len(f.sectionList))
copy(list, f.sectionList)
return list
}
// DeleteSection deletes a section.
func (f *File) DeleteSection(name string) {
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
if len(name) == 0 {
name = DEFAULT_SECTION
}
for i, s := range f.sectionList {
if s == name {
f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...)
delete(f.sections, name)
return
}
}
}
func (f *File) reload(s dataSource) error {
r, err := s.ReadCloser()
if err != nil {
return err
}
defer r.Close()
return f.parse(r)
}
// Reload reloads and parses all data sources.
func (f *File) Reload() (err error) {
for _, s := range f.dataSources {
if err = f.reload(s); err != nil {
// In loose mode, we create an empty default section for nonexistent files.
if os.IsNotExist(err) && f.options.Loose {
f.parse(bytes.NewBuffer(nil))
continue
}
return err
}
}
return nil
}
// Append appends one or more data sources and reloads automatically.
func (f *File) Append(source interface{}, others ...interface{}) error {
ds, err := parseDataSource(source)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
for _, s := range others {
ds, err = parseDataSource(s)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
}
return f.Reload()
}
func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
equalSign := "="
if PrettyFormat || PrettyEqual {
equalSign = " = "
}
// Use buffer to make sure target is safe until finish encoding.
buf := bytes.NewBuffer(nil)
for i, sname := range f.sectionList {
sec := f.Section(sname)
if len(sec.Comment) > 0 {
if sec.Comment[0] != '#' && sec.Comment[0] != ';' {
sec.Comment = "; " + sec.Comment
} else {
sec.Comment = sec.Comment[:1] + " " + strings.TrimSpace(sec.Comment[1:])
}
if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil {
return nil, err
}
}
if i > 0 || DefaultHeader {
if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
return nil, err
}
} else {
// Write nothing if default section is empty
if len(sec.keyList) == 0 {
continue
}
}
if sec.isRawSection {
if _, err := buf.WriteString(sec.rawBody); err != nil {
return nil, err
}
if PrettySection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
continue
}
// Count and generate alignment length and buffer spaces using the
// longest key. Keys may be modifed if they contain certain characters so
// we need to take that into account in our calculation.
alignLength := 0
if PrettyFormat {
for _, kname := range sec.keyList {
keyLength := len(kname)
// First case will surround key by ` and second by """
if strings.ContainsAny(kname, "\"=:") {
keyLength += 2
} else if strings.Contains(kname, "`") {
keyLength += 6
}
if keyLength > alignLength {
alignLength = keyLength
}
}
}
alignSpaces := bytes.Repeat([]byte(" "), alignLength)
KEY_LIST:
for _, kname := range sec.keyList {
key := sec.Key(kname)
if len(key.Comment) > 0 {
if len(indent) > 0 && sname != DEFAULT_SECTION {
buf.WriteString(indent)
}
if key.Comment[0] != '#' && key.Comment[0] != ';' {
key.Comment = "; " + key.Comment
} else {
key.Comment = key.Comment[:1] + " " + strings.TrimSpace(key.Comment[1:])
}
// Support multiline comments
key.Comment = strings.Replace(key.Comment, "\n", "\n; ", -1)
if _, err := buf.WriteString(key.Comment + LineBreak); err != nil {
return nil, err
}
}
if len(indent) > 0 && sname != DEFAULT_SECTION {
buf.WriteString(indent)
}
switch {
case key.isAutoIncrement:
kname = "-"
case strings.ContainsAny(kname, "\"=:"):
kname = "`" + kname + "`"
case strings.Contains(kname, "`"):
kname = `"""` + kname + `"""`
}
for _, val := range key.ValueWithShadows() {
if _, err := buf.WriteString(kname); err != nil {
return nil, err
}
if key.isBooleanType {
if kname != sec.keyList[len(sec.keyList)-1] {
buf.WriteString(LineBreak)
}
continue KEY_LIST
}
// Write out alignment spaces before "=" sign
if PrettyFormat {
buf.Write(alignSpaces[:alignLength-len(kname)])
}
// In case key value contains "\n", "`", "\"", "#" or ";"
if strings.ContainsAny(val, "\n`") {
val = `"""` + val + `"""`
} else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") {
val = "`" + val + "`"
}
if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil {
return nil, err
}
}
for _, val := range key.nestedValues {
if _, err := buf.WriteString(indent + " " + val + LineBreak); err != nil {
return nil, err
}
}
}
if PrettySection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
}
return buf, nil
}
// WriteToIndent writes content into io.Writer with given indention.
// If PrettyFormat has been set to be true,
// it will align "=" sign with spaces under each section.
func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) {
buf, err := f.writeToBuffer(indent)
if err != nil {
return 0, err
}
return buf.WriteTo(w)
}
// WriteTo writes file content into io.Writer.
func (f *File) WriteTo(w io.Writer) (int64, error) {
return f.WriteToIndent(w, "")
}
// SaveToIndent writes content to file system with given value indention.
func (f *File) SaveToIndent(filename, indent string) error {
// Note: Because we are truncating with os.Create,
// so it's safer to save to a temporary file location and rename afte done.
buf, err := f.writeToBuffer(indent)
if err != nil {
return err
}
return ioutil.WriteFile(filename, buf.Bytes(), 0666)
}
// SaveTo writes content to file system.
func (f *File) SaveTo(filename string) error {
return f.SaveToIndent(filename, "")
}

202
vendor/github.com/go-ini/ini/ini.go generated vendored
View File

@ -1,202 +0,0 @@
// Copyright 2014 Unknwon
//
// 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.
// Package ini provides INI file read and write functionality in Go.
package ini
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"regexp"
"runtime"
)
const (
// Name for default section. You can use this constant or the string literal.
// In most of cases, an empty string is all you need to access the section.
DEFAULT_SECTION = "DEFAULT"
// Maximum allowed depth when recursively substituing variable names.
_DEPTH_VALUES = 99
_VERSION = "1.36.0"
)
// Version returns current package version literal.
func Version() string {
return _VERSION
}
var (
// Delimiter to determine or compose a new line.
// This variable will be changed to "\r\n" automatically on Windows
// at package init time.
LineBreak = "\n"
// Variable regexp pattern: %(variable)s
varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`)
// Indicate whether to align "=" sign with spaces to produce pretty output
// or reduce all possible spaces for compact format.
PrettyFormat = true
// Place spaces around "=" sign even when PrettyFormat is false
PrettyEqual = false
// Explicitly write DEFAULT section header
DefaultHeader = false
// Indicate whether to put a line between sections
PrettySection = true
)
func init() {
if runtime.GOOS == "windows" {
LineBreak = "\r\n"
}
}
func inSlice(str string, s []string) bool {
for _, v := range s {
if str == v {
return true
}
}
return false
}
// dataSource is an interface that returns object which can be read and closed.
type dataSource interface {
ReadCloser() (io.ReadCloser, error)
}
// sourceFile represents an object that contains content on the local file system.
type sourceFile struct {
name string
}
func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) {
return os.Open(s.name)
}
// sourceData represents an object that contains content in memory.
type sourceData struct {
data []byte
}
func (s *sourceData) ReadCloser() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(s.data)), nil
}
// sourceReadCloser represents an input stream with Close method.
type sourceReadCloser struct {
reader io.ReadCloser
}
func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) {
return s.reader, nil
}
func parseDataSource(source interface{}) (dataSource, error) {
switch s := source.(type) {
case string:
return sourceFile{s}, nil
case []byte:
return &sourceData{s}, nil
case io.ReadCloser:
return &sourceReadCloser{s}, nil
default:
return nil, fmt.Errorf("error parsing data source: unknown type '%s'", s)
}
}
type LoadOptions struct {
// Loose indicates whether the parser should ignore nonexistent files or return error.
Loose bool
// Insensitive indicates whether the parser forces all section and key names to lowercase.
Insensitive bool
// IgnoreContinuation indicates whether to ignore continuation lines while parsing.
IgnoreContinuation bool
// IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value.
IgnoreInlineComment bool
// AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing.
// This type of keys are mostly used in my.cnf.
AllowBooleanKeys bool
// AllowShadows indicates whether to keep track of keys with same name under same section.
AllowShadows bool
// AllowNestedValues indicates whether to allow AWS-like nested values.
// Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values
AllowNestedValues bool
// AllowPythonMultilineValues indicates whether to allow Python-like multi-line values.
// Docs: https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
// Relevant quote: Values can also span multiple lines, as long as they are indented deeper
// than the first line of the value.
AllowPythonMultilineValues bool
// UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format
// when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value"
UnescapeValueDoubleQuotes bool
// UnescapeValueCommentSymbols indicates to unescape comment symbols (\# and \;) inside value to regular format
// when value is NOT surrounded by any quotes.
// Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all.
UnescapeValueCommentSymbols bool
// Some INI formats allow group blocks that store a block of raw content that doesn't otherwise
// conform to key/value pairs. Specify the names of those blocks here.
UnparseableSections []string
}
func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) {
sources := make([]dataSource, len(others)+1)
sources[0], err = parseDataSource(source)
if err != nil {
return nil, err
}
for i := range others {
sources[i+1], err = parseDataSource(others[i])
if err != nil {
return nil, err
}
}
f := newFile(sources, opts)
if err = f.Reload(); err != nil {
return nil, err
}
return f, nil
}
// Load loads and parses from INI data sources.
// Arguments can be mixed of file name with string type, or raw data in []byte.
// It will return error if list contains nonexistent files.
func Load(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{}, source, others...)
}
// LooseLoad has exactly same functionality as Load function
// except it ignores nonexistent files instead of returning error.
func LooseLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{Loose: true}, source, others...)
}
// InsensitiveLoad has exactly same functionality as Load function
// except it forces all section and key names to be lowercased.
func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{Insensitive: true}, source, others...)
}
// InsensitiveLoad has exactly same functionality as Load function
// except it allows have shadow keys.
func ShadowLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{AllowShadows: true}, source, others...)
}

751
vendor/github.com/go-ini/ini/key.go generated vendored
View File

@ -1,751 +0,0 @@
// Copyright 2014 Unknwon
//
// 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.
package ini
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
// Key represents a key under a section.
type Key struct {
s *Section
Comment string
name string
value string
isAutoIncrement bool
isBooleanType bool
isShadow bool
shadows []*Key
nestedValues []string
}
// newKey simply return a key object with given values.
func newKey(s *Section, name, val string) *Key {
return &Key{
s: s,
name: name,
value: val,
}
}
func (k *Key) addShadow(val string) error {
if k.isShadow {
return errors.New("cannot add shadow to another shadow key")
} else if k.isAutoIncrement || k.isBooleanType {
return errors.New("cannot add shadow to auto-increment or boolean key")
}
shadow := newKey(k.s, k.name, val)
shadow.isShadow = true
k.shadows = append(k.shadows, shadow)
return nil
}
// AddShadow adds a new shadow key to itself.
func (k *Key) AddShadow(val string) error {
if !k.s.f.options.AllowShadows {
return errors.New("shadow key is not allowed")
}
return k.addShadow(val)
}
func (k *Key) addNestedValue(val string) error {
if k.isAutoIncrement || k.isBooleanType {
return errors.New("cannot add nested value to auto-increment or boolean key")
}
k.nestedValues = append(k.nestedValues, val)
return nil
}
func (k *Key) AddNestedValue(val string) error {
if !k.s.f.options.AllowNestedValues {
return errors.New("nested value is not allowed")
}
return k.addNestedValue(val)
}
// ValueMapper represents a mapping function for values, e.g. os.ExpandEnv
type ValueMapper func(string) string
// Name returns name of key.
func (k *Key) Name() string {
return k.name
}
// Value returns raw value of key for performance purpose.
func (k *Key) Value() string {
return k.value
}
// ValueWithShadows returns raw values of key and its shadows if any.
func (k *Key) ValueWithShadows() []string {
if len(k.shadows) == 0 {
return []string{k.value}
}
vals := make([]string, len(k.shadows)+1)
vals[0] = k.value
for i := range k.shadows {
vals[i+1] = k.shadows[i].value
}
return vals
}
// NestedValues returns nested values stored in the key.
// It is possible returned value is nil if no nested values stored in the key.
func (k *Key) NestedValues() []string {
return k.nestedValues
}
// transformValue takes a raw value and transforms to its final string.
func (k *Key) transformValue(val string) string {
if k.s.f.ValueMapper != nil {
val = k.s.f.ValueMapper(val)
}
// Fail-fast if no indicate char found for recursive value
if !strings.Contains(val, "%") {
return val
}
for i := 0; i < _DEPTH_VALUES; i++ {
vr := varPattern.FindString(val)
if len(vr) == 0 {
break
}
// Take off leading '%(' and trailing ')s'.
noption := strings.TrimLeft(vr, "%(")
noption = strings.TrimRight(noption, ")s")
// Search in the same section.
nk, err := k.s.GetKey(noption)
if err != nil || k == nk {
// Search again in default section.
nk, _ = k.s.f.Section("").GetKey(noption)
}
// Substitute by new value and take off leading '%(' and trailing ')s'.
val = strings.Replace(val, vr, nk.value, -1)
}
return val
}
// String returns string representation of value.
func (k *Key) String() string {
return k.transformValue(k.value)
}
// Validate accepts a validate function which can
// return modifed result as key value.
func (k *Key) Validate(fn func(string) string) string {
return fn(k.String())
}
// parseBool returns the boolean value represented by the string.
//
// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On,
// 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off.
// Any other value returns an error.
func parseBool(str string) (value bool, err error) {
switch str {
case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On":
return true, nil
case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off":
return false, nil
}
return false, fmt.Errorf("parsing \"%s\": invalid syntax", str)
}
// Bool returns bool type value.
func (k *Key) Bool() (bool, error) {
return parseBool(k.String())
}
// Float64 returns float64 type value.
func (k *Key) Float64() (float64, error) {
return strconv.ParseFloat(k.String(), 64)
}
// Int returns int type value.
func (k *Key) Int() (int, error) {
return strconv.Atoi(k.String())
}
// Int64 returns int64 type value.
func (k *Key) Int64() (int64, error) {
return strconv.ParseInt(k.String(), 10, 64)
}
// Uint returns uint type valued.
func (k *Key) Uint() (uint, error) {
u, e := strconv.ParseUint(k.String(), 10, 64)
return uint(u), e
}
// Uint64 returns uint64 type value.
func (k *Key) Uint64() (uint64, error) {
return strconv.ParseUint(k.String(), 10, 64)
}
// Duration returns time.Duration type value.
func (k *Key) Duration() (time.Duration, error) {
return time.ParseDuration(k.String())
}
// TimeFormat parses with given format and returns time.Time type value.
func (k *Key) TimeFormat(format string) (time.Time, error) {
return time.Parse(format, k.String())
}
// Time parses with RFC3339 format and returns time.Time type value.
func (k *Key) Time() (time.Time, error) {
return k.TimeFormat(time.RFC3339)
}
// MustString returns default value if key value is empty.
func (k *Key) MustString(defaultVal string) string {
val := k.String()
if len(val) == 0 {
k.value = defaultVal
return defaultVal
}
return val
}
// MustBool always returns value without error,
// it returns false if error occurs.
func (k *Key) MustBool(defaultVal ...bool) bool {
val, err := k.Bool()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatBool(defaultVal[0])
return defaultVal[0]
}
return val
}
// MustFloat64 always returns value without error,
// it returns 0.0 if error occurs.
func (k *Key) MustFloat64(defaultVal ...float64) float64 {
val, err := k.Float64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64)
return defaultVal[0]
}
return val
}
// MustInt always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustInt(defaultVal ...int) int {
val, err := k.Int()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatInt(int64(defaultVal[0]), 10)
return defaultVal[0]
}
return val
}
// MustInt64 always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustInt64(defaultVal ...int64) int64 {
val, err := k.Int64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatInt(defaultVal[0], 10)
return defaultVal[0]
}
return val
}
// MustUint always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustUint(defaultVal ...uint) uint {
val, err := k.Uint()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatUint(uint64(defaultVal[0]), 10)
return defaultVal[0]
}
return val
}
// MustUint64 always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustUint64(defaultVal ...uint64) uint64 {
val, err := k.Uint64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatUint(defaultVal[0], 10)
return defaultVal[0]
}
return val
}
// MustDuration always returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration {
val, err := k.Duration()
if len(defaultVal) > 0 && err != nil {
k.value = defaultVal[0].String()
return defaultVal[0]
}
return val
}
// MustTimeFormat always parses with given format and returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time {
val, err := k.TimeFormat(format)
if len(defaultVal) > 0 && err != nil {
k.value = defaultVal[0].Format(format)
return defaultVal[0]
}
return val
}
// MustTime always parses with RFC3339 format and returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustTime(defaultVal ...time.Time) time.Time {
return k.MustTimeFormat(time.RFC3339, defaultVal...)
}
// In always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) In(defaultVal string, candidates []string) string {
val := k.String()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InFloat64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 {
val := k.MustFloat64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InInt always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InInt(defaultVal int, candidates []int) int {
val := k.MustInt()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InInt64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 {
val := k.MustInt64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InUint always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InUint(defaultVal uint, candidates []uint) uint {
val := k.MustUint()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InUint64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 {
val := k.MustUint64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InTimeFormat always parses with given format and returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time {
val := k.MustTimeFormat(format)
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InTime always parses with RFC3339 format and returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time {
return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
}
// RangeFloat64 checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 {
val := k.MustFloat64()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeInt checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeInt(defaultVal, min, max int) int {
val := k.MustInt()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeInt64 checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeInt64(defaultVal, min, max int64) int64 {
val := k.MustInt64()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeTimeFormat checks if value with given format is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time {
val := k.MustTimeFormat(format)
if val.Unix() < min.Unix() || val.Unix() > max.Unix() {
return defaultVal
}
return val
}
// RangeTime checks if value with RFC3339 format is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time {
return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max)
}
// Strings returns list of string divided by given delimiter.
func (k *Key) Strings(delim string) []string {
str := k.String()
if len(str) == 0 {
return []string{}
}
runes := []rune(str)
vals := make([]string, 0, 2)
var buf bytes.Buffer
escape := false
idx := 0
for {
if escape {
escape = false
if runes[idx] != '\\' && !strings.HasPrefix(string(runes[idx:]), delim) {
buf.WriteRune('\\')
}
buf.WriteRune(runes[idx])
} else {
if runes[idx] == '\\' {
escape = true
} else if strings.HasPrefix(string(runes[idx:]), delim) {
idx += len(delim) - 1
vals = append(vals, strings.TrimSpace(buf.String()))
buf.Reset()
} else {
buf.WriteRune(runes[idx])
}
}
idx += 1
if idx == len(runes) {
break
}
}
if buf.Len() > 0 {
vals = append(vals, strings.TrimSpace(buf.String()))
}
return vals
}
// StringsWithShadows returns list of string divided by given delimiter.
// Shadows will also be appended if any.
func (k *Key) StringsWithShadows(delim string) []string {
vals := k.ValueWithShadows()
results := make([]string, 0, len(vals)*2)
for i := range vals {
if len(vals) == 0 {
continue
}
results = append(results, strings.Split(vals[i], delim)...)
}
for i := range results {
results[i] = k.transformValue(strings.TrimSpace(results[i]))
}
return results
}
// Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Float64s(delim string) []float64 {
vals, _ := k.parseFloat64s(k.Strings(delim), true, false)
return vals
}
// Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Ints(delim string) []int {
vals, _ := k.parseInts(k.Strings(delim), true, false)
return vals
}
// Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Int64s(delim string) []int64 {
vals, _ := k.parseInt64s(k.Strings(delim), true, false)
return vals
}
// Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Uints(delim string) []uint {
vals, _ := k.parseUints(k.Strings(delim), true, false)
return vals
}
// Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Uint64s(delim string) []uint64 {
vals, _ := k.parseUint64s(k.Strings(delim), true, false)
return vals
}
// TimesFormat parses with given format and returns list of time.Time divided by given delimiter.
// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
func (k *Key) TimesFormat(format, delim string) []time.Time {
vals, _ := k.parseTimesFormat(format, k.Strings(delim), true, false)
return vals
}
// Times parses with RFC3339 format and returns list of time.Time divided by given delimiter.
// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
func (k *Key) Times(delim string) []time.Time {
return k.TimesFormat(time.RFC3339, delim)
}
// ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then
// it will not be included to result list.
func (k *Key) ValidFloat64s(delim string) []float64 {
vals, _ := k.parseFloat64s(k.Strings(delim), false, false)
return vals
}
// ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will
// not be included to result list.
func (k *Key) ValidInts(delim string) []int {
vals, _ := k.parseInts(k.Strings(delim), false, false)
return vals
}
// ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer,
// then it will not be included to result list.
func (k *Key) ValidInt64s(delim string) []int64 {
vals, _ := k.parseInt64s(k.Strings(delim), false, false)
return vals
}
// ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer,
// then it will not be included to result list.
func (k *Key) ValidUints(delim string) []uint {
vals, _ := k.parseUints(k.Strings(delim), false, false)
return vals
}
// ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned
// integer, then it will not be included to result list.
func (k *Key) ValidUint64s(delim string) []uint64 {
vals, _ := k.parseUint64s(k.Strings(delim), false, false)
return vals
}
// ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter.
func (k *Key) ValidTimesFormat(format, delim string) []time.Time {
vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false)
return vals
}
// ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter.
func (k *Key) ValidTimes(delim string) []time.Time {
return k.ValidTimesFormat(time.RFC3339, delim)
}
// StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictFloat64s(delim string) ([]float64, error) {
return k.parseFloat64s(k.Strings(delim), false, true)
}
// StrictInts returns list of int divided by given delimiter or error on first invalid input.
func (k *Key) StrictInts(delim string) ([]int, error) {
return k.parseInts(k.Strings(delim), false, true)
}
// StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictInt64s(delim string) ([]int64, error) {
return k.parseInt64s(k.Strings(delim), false, true)
}
// StrictUints returns list of uint divided by given delimiter or error on first invalid input.
func (k *Key) StrictUints(delim string) ([]uint, error) {
return k.parseUints(k.Strings(delim), false, true)
}
// StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictUint64s(delim string) ([]uint64, error) {
return k.parseUint64s(k.Strings(delim), false, true)
}
// StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter
// or error on first invalid input.
func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) {
return k.parseTimesFormat(format, k.Strings(delim), false, true)
}
// StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter
// or error on first invalid input.
func (k *Key) StrictTimes(delim string) ([]time.Time, error) {
return k.StrictTimesFormat(time.RFC3339, delim)
}
// parseFloat64s transforms strings to float64s.
func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) {
vals := make([]float64, 0, len(strs))
for _, str := range strs {
val, err := strconv.ParseFloat(str, 64)
if err != nil && returnOnInvalid {
return nil, err
}
if err == nil || addInvalid {
vals = append(vals, val)
}
}
return vals, nil
}
// parseInts transforms strings to ints.
func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) {
vals := make([]int, 0, len(strs))
for _, str := range strs {
val, err := strconv.Atoi(str)
if err != nil && returnOnInvalid {
return nil, err
}
if err == nil || addInvalid {
vals = append(vals, val)
}
}
return vals, nil
}
// parseInt64s transforms strings to int64s.
func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) {
vals := make([]int64, 0, len(strs))
for _, str := range strs {
val, err := strconv.ParseInt(str, 10, 64)
if err != nil && returnOnInvalid {
return nil, err
}
if err == nil || addInvalid {
vals = append(vals, val)
}
}
return vals, nil
}
// parseUints transforms strings to uints.
func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) {
vals := make([]uint, 0, len(strs))
for _, str := range strs {
val, err := strconv.ParseUint(str, 10, 0)
if err != nil && returnOnInvalid {
return nil, err
}
if err == nil || addInvalid {
vals = append(vals, uint(val))
}
}
return vals, nil
}
// parseUint64s transforms strings to uint64s.
func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) {
vals := make([]uint64, 0, len(strs))
for _, str := range strs {
val, err := strconv.ParseUint(str, 10, 64)
if err != nil && returnOnInvalid {
return nil, err
}
if err == nil || addInvalid {
vals = append(vals, val)
}
}
return vals, nil
}
// parseTimesFormat transforms strings to times in given format.
func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) {
vals := make([]time.Time, 0, len(strs))
for _, str := range strs {
val, err := time.Parse(format, str)
if err != nil && returnOnInvalid {
return nil, err
}
if err == nil || addInvalid {
vals = append(vals, val)
}
}
return vals, nil
}
// SetValue changes key value.
func (k *Key) SetValue(v string) {
if k.s.f.BlockMode {
k.s.f.lock.Lock()
defer k.s.f.lock.Unlock()
}
k.value = v
k.s.keysHash[k.name] = v
}

View File

@ -1,477 +0,0 @@
// Copyright 2015 Unknwon
//
// 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.
package ini
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"unicode"
)
var pythonMultiline = regexp.MustCompile("^(\\s+)([^\n]+)")
type tokenType int
const (
_TOKEN_INVALID tokenType = iota
_TOKEN_COMMENT
_TOKEN_SECTION
_TOKEN_KEY
)
type parser struct {
buf *bufio.Reader
isEOF bool
count int
comment *bytes.Buffer
}
func newParser(r io.Reader) *parser {
return &parser{
buf: bufio.NewReader(r),
count: 1,
comment: &bytes.Buffer{},
}
}
// BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format.
// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
func (p *parser) BOM() error {
mask, err := p.buf.Peek(2)
if err != nil && err != io.EOF {
return err
} else if len(mask) < 2 {
return nil
}
switch {
case mask[0] == 254 && mask[1] == 255:
fallthrough
case mask[0] == 255 && mask[1] == 254:
p.buf.Read(mask)
case mask[0] == 239 && mask[1] == 187:
mask, err := p.buf.Peek(3)
if err != nil && err != io.EOF {
return err
} else if len(mask) < 3 {
return nil
}
if mask[2] == 191 {
p.buf.Read(mask)
}
}
return nil
}
func (p *parser) readUntil(delim byte) ([]byte, error) {
data, err := p.buf.ReadBytes(delim)
if err != nil {
if err == io.EOF {
p.isEOF = true
} else {
return nil, err
}
}
return data, nil
}
func cleanComment(in []byte) ([]byte, bool) {
i := bytes.IndexAny(in, "#;")
if i == -1 {
return nil, false
}
return in[i:], true
}
func readKeyName(in []byte) (string, int, error) {
line := string(in)
// Check if key name surrounded by quotes.
var keyQuote string
if line[0] == '"' {
if len(line) > 6 && string(line[0:3]) == `"""` {
keyQuote = `"""`
} else {
keyQuote = `"`
}
} else if line[0] == '`' {
keyQuote = "`"
}
// Get out key name
endIdx := -1
if len(keyQuote) > 0 {
startIdx := len(keyQuote)
// FIXME: fail case -> """"""name"""=value
pos := strings.Index(line[startIdx:], keyQuote)
if pos == -1 {
return "", -1, fmt.Errorf("missing closing key quote: %s", line)
}
pos += startIdx
// Find key-value delimiter
i := strings.IndexAny(line[pos+startIdx:], "=:")
if i < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
endIdx = pos + i
return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
}
endIdx = strings.IndexAny(line, "=:")
if endIdx < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil
}
func (p *parser) readMultilines(line, val, valQuote string) (string, error) {
for {
data, err := p.readUntil('\n')
if err != nil {
return "", err
}
next := string(data)
pos := strings.LastIndex(next, valQuote)
if pos > -1 {
val += next[:pos]
comment, has := cleanComment([]byte(next[pos:]))
if has {
p.comment.Write(bytes.TrimSpace(comment))
}
break
}
val += next
if p.isEOF {
return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next)
}
}
return val, nil
}
func (p *parser) readContinuationLines(val string) (string, error) {
for {
data, err := p.readUntil('\n')
if err != nil {
return "", err
}
next := strings.TrimSpace(string(data))
if len(next) == 0 {
break
}
val += next
if val[len(val)-1] != '\\' {
break
}
val = val[:len(val)-1]
}
return val, nil
}
// hasSurroundedQuote check if and only if the first and last characters
// are quotes \" or \'.
// It returns false if any other parts also contain same kind of quotes.
func hasSurroundedQuote(in string, quote byte) bool {
return len(in) >= 2 && in[0] == quote && in[len(in)-1] == quote &&
strings.IndexByte(in[1:], quote) == len(in)-2
}
func (p *parser) readValue(in []byte,
parserBufferSize int,
ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols, allowPythonMultilines bool) (string, error) {
line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
if len(line) == 0 {
return "", nil
}
var valQuote string
if len(line) > 3 && string(line[0:3]) == `"""` {
valQuote = `"""`
} else if line[0] == '`' {
valQuote = "`"
} else if unescapeValueDoubleQuotes && line[0] == '"' {
valQuote = `"`
}
if len(valQuote) > 0 {
startIdx := len(valQuote)
pos := strings.LastIndex(line[startIdx:], valQuote)
// Check for multi-line value
if pos == -1 {
return p.readMultilines(line, line[startIdx:], valQuote)
}
if unescapeValueDoubleQuotes && valQuote == `"` {
return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil
}
return line[startIdx : pos+startIdx], nil
}
lastChar := line[len(line)-1]
// Won't be able to reach here if value only contains whitespace
line = strings.TrimSpace(line)
trimmedLastChar := line[len(line)-1]
// Check continuation lines when desired
if !ignoreContinuation && trimmedLastChar == '\\' {
return p.readContinuationLines(line[:len(line)-1])
}
// Check if ignore inline comment
if !ignoreInlineComment {
i := strings.IndexAny(line, "#;")
if i > -1 {
p.comment.WriteString(line[i:])
line = strings.TrimSpace(line[:i])
}
}
// Trim single and double quotes
if hasSurroundedQuote(line, '\'') ||
hasSurroundedQuote(line, '"') {
line = line[1 : len(line)-1]
} else if len(valQuote) == 0 && unescapeValueCommentSymbols {
if strings.Contains(line, `\;`) {
line = strings.Replace(line, `\;`, ";", -1)
}
if strings.Contains(line, `\#`) {
line = strings.Replace(line, `\#`, "#", -1)
}
} else if allowPythonMultilines && lastChar == '\n' {
parserBufferPeekResult, _ := p.buf.Peek(parserBufferSize)
peekBuffer := bytes.NewBuffer(parserBufferPeekResult)
identSize := -1
val := line
for {
peekData, peekErr := peekBuffer.ReadBytes('\n')
if peekErr != nil {
if peekErr == io.EOF {
return val, nil
}
return "", peekErr
}
peekMatches := pythonMultiline.FindStringSubmatch(string(peekData))
if len(peekMatches) != 3 {
return val, nil
}
currentIdentSize := len(peekMatches[1])
// NOTE: Return if not a python-ini multi-line value.
if currentIdentSize < 0 {
return val, nil
}
identSize = currentIdentSize
// NOTE: Just advance the parser reader (buffer) in-sync with the peek buffer.
_, err := p.readUntil('\n')
if err != nil {
return "", err
}
val += fmt.Sprintf("\n%s", peekMatches[2])
}
// NOTE: If it was a Python multi-line value,
// return the appended value.
if identSize > 0 {
return val, nil
}
}
return line, nil
}
// parse parses data through an io.Reader.
func (f *File) parse(reader io.Reader) (err error) {
p := newParser(reader)
if err = p.BOM(); err != nil {
return fmt.Errorf("BOM: %v", err)
}
// Ignore error because default section name is never empty string.
name := DEFAULT_SECTION
if f.options.Insensitive {
name = strings.ToLower(DEFAULT_SECTION)
}
section, _ := f.NewSection(name)
// This "last" is not strictly equivalent to "previous one" if current key is not the first nested key
var isLastValueEmpty bool
var lastRegularKey *Key
var line []byte
var inUnparseableSection bool
// NOTE: Iterate and increase `currentPeekSize` until
// the size of the parser buffer is found.
// TODO: When Golang 1.10 is the lowest version supported,
// replace with `parserBufferSize := p.buf.Size()`.
parserBufferSize := 0
// NOTE: Peek 1kb at a time.
currentPeekSize := 1024
if f.options.AllowPythonMultilineValues {
for {
peekBytes, _ := p.buf.Peek(currentPeekSize)
peekBytesLength := len(peekBytes)
if parserBufferSize >= peekBytesLength {
break
}
currentPeekSize *= 2
parserBufferSize = peekBytesLength
}
}
for !p.isEOF {
line, err = p.readUntil('\n')
if err != nil {
return err
}
if f.options.AllowNestedValues &&
isLastValueEmpty && len(line) > 0 {
if line[0] == ' ' || line[0] == '\t' {
lastRegularKey.addNestedValue(string(bytes.TrimSpace(line)))
continue
}
}
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
if len(line) == 0 {
continue
}
// Comments
if line[0] == '#' || line[0] == ';' {
// Note: we do not care ending line break,
// it is needed for adding second line,
// so just clean it once at the end when set to value.
p.comment.Write(line)
continue
}
// Section
if line[0] == '[' {
// Read to the next ']' (TODO: support quoted strings)
// TODO(unknwon): use LastIndexByte when stop supporting Go1.4
closeIdx := bytes.LastIndex(line, []byte("]"))
if closeIdx == -1 {
return fmt.Errorf("unclosed section: %s", line)
}
name := string(line[1:closeIdx])
section, err = f.NewSection(name)
if err != nil {
return err
}
comment, has := cleanComment(line[closeIdx+1:])
if has {
p.comment.Write(comment)
}
section.Comment = strings.TrimSpace(p.comment.String())
// Reset aotu-counter and comments
p.comment.Reset()
p.count = 1
inUnparseableSection = false
for i := range f.options.UnparseableSections {
if f.options.UnparseableSections[i] == name ||
(f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) {
inUnparseableSection = true
continue
}
}
continue
}
if inUnparseableSection {
section.isRawSection = true
section.rawBody += string(line)
continue
}
kname, offset, err := readKeyName(line)
if err != nil {
// Treat as boolean key when desired, and whole line is key name.
if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys {
kname, err := p.readValue(line,
parserBufferSize,
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols,
f.options.AllowPythonMultilineValues)
if err != nil {
return err
}
key, err := section.NewBooleanKey(kname)
if err != nil {
return err
}
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
continue
}
return err
}
// Auto increment.
isAutoIncr := false
if kname == "-" {
isAutoIncr = true
kname = "#" + strconv.Itoa(p.count)
p.count++
}
value, err := p.readValue(line[offset:],
parserBufferSize,
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols,
f.options.AllowPythonMultilineValues)
if err != nil {
return err
}
isLastValueEmpty = len(value) == 0
key, err := section.NewKey(kname, value)
if err != nil {
return err
}
key.isAutoIncrement = isAutoIncr
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
lastRegularKey = key
}
return nil
}

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