Merge branch 'master' into flip-table-graph-feature-flag
commit
685d9ce688
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.4.2.3
|
||||
current_version = 1.4.3.0
|
||||
files = README.md server/swagger.json
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
|
||||
serialize = {major}.{minor}.{patch}.{release}
|
||||
|
|
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -1,13 +1,23 @@
|
|||
## v1.5.0.0 [unreleased]
|
||||
|
||||
### Features
|
||||
|
||||
1. [#2526](https://github.com/influxdata/chronograf/pull/2526): Add support for RS256/JWKS verification, support for id_token parsing (as in ADFS)
|
||||
1. [#3080](https://github.com/influxdata/chronograf/pull/3080): Add tabular data visualization option with features
|
||||
1. [#3103](https://github.com/influxdata/chronograf/pull/3103): Add ability to clone dashboards
|
||||
|
||||
### UI Improvements
|
||||
|
||||
1. [#3088](https://github.com/influxdata/chronograf/pull/3088): New dashboard cells appear at bottom of layout and assume the size of the most common cell
|
||||
1. [#3096](https://github.com/influxdata/chronograf/pull/3096): Standardize delete confirmation interactions
|
||||
1. [#3096](https://github.com/influxdata/chronograf/pull/3096): Standardize save & cancel interactions
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
## v1.4.3.0 [unreleased]
|
||||
1. [#2950](https://github.com/influxdata/chronograf/pull/2094): Always save template variables on first edit
|
||||
1. [#3101](https://github.com/influxdata/chronograf/pull/3101): Fix template variables not loading
|
||||
|
||||
## v1.4.3.0 [2018-3-28]
|
||||
|
||||
### Features
|
||||
|
||||
|
@ -32,6 +42,10 @@
|
|||
1. [#3068](https://github.com/influxdata/chronograf/pull/3068): Fix intermittent missing fill from graphs
|
||||
1. [#3087](https://github.com/influxdata/chronograf/pull/3087): Exit annotation edit mode when user navigates away from dashboard
|
||||
1. [#3079](https://github.com/influxdata/chronograf/pull/3082): Support custom time range in annotations api wrapper
|
||||
1. [#3068](https://github.com/influxdata/chronograf/pull/3068): Fix intermittent missing fill from graphs
|
||||
1. [#3079](https://github.com/influxdata/chronograf/pull/3082): Support custom time range in annotations api wrapper
|
||||
1. [#3087](https://github.com/influxdata/chronograf/pull/3087): Exit annotation edit mode when user navigates away from dashboard
|
||||
1. [#3073](https://github.com/influxdata/chronograf/pull/3073): Fix Delete button in All Users admin page
|
||||
|
||||
## v1.4.2.3 [2018-03-08]
|
||||
|
||||
|
@ -39,14 +53,13 @@
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
1. [#2866](https://github.com/influxdata/chronograf/pull/2866): Change hover text on delete mappings confirmation button to 'Delete'
|
||||
1. [#2911](https://github.com/influxdata/chronograf/pull/2911): Fix Heroku OAuth
|
||||
1. [#2859](https://github.com/influxdata/chronograf/pull/2859): Enable Mappings save button when valid
|
||||
1. [#2933](https://github.com/influxdata/chronograf/pull/2933): Include url in Kapacitor connection creation requests
|
||||
|
||||
## v1.4.2.1 [2018-02-28]
|
||||
|
||||
### Features
|
||||
|
||||
1. [#2837](https://github.com/influxdata/chronograf/pull/2837): Prevent execution of queries in cells that are not in view on the dashboard page
|
||||
1. [#2829](https://github.com/influxdata/chronograf/pull/2829): Add an optional persistent legend which can toggle series visibility to dashboard cells
|
||||
1. [#2846](https://github.com/influxdata/chronograf/pull/2846): Allow user to annotate graphs via UI or API
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
Contributing to Chronograf
|
||||
==========================
|
||||
# Contributing to Chronograf
|
||||
|
||||
## Bug reports
|
||||
|
||||
Bug reports
|
||||
---------------
|
||||
Before you file an issue, please search existing issues in case it has already been filed, or perhaps even fixed. If you file an issue, please include the following.
|
||||
|
||||
* Full details of your operating system (or distribution) e.g. 64-bit Ubuntu 14.04.
|
||||
* The version of Chronograf you are running
|
||||
* Whether you installed it using a pre-built package, or built it from source.
|
||||
|
@ -13,77 +13,65 @@ Before you file an issue, please search existing issues in case it has already b
|
|||
Remember the golden rule of bug reports: **The easier you make it for us to reproduce the problem, the faster it will get fixed.**
|
||||
If you have never written a bug report before, or if you want to brush up on your bug reporting skills, we recommend reading [Simon Tatham's essay "How to Report Bugs Effectively."](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html)
|
||||
|
||||
Please note that issues are *not the place to file general questions* such as "How do I use Chronograf?" Questions of this nature should be sent to the [InfluxDB Google Group](https://groups.google.com/forum/#!forum/influxdb), not filed as issues. Issues like this will be closed.
|
||||
Please note that issues are _not the place to file general questions_ such as "How do I use Chronograf?" Questions of this nature should be sent to the [InfluxDB Google Group](https://groups.google.com/forum/#!forum/influxdb), not filed as issues. Issues like this will be closed.
|
||||
|
||||
## Feature requests
|
||||
|
||||
Feature requests
|
||||
----------------
|
||||
We really like to receive feature requests, as it helps us prioritize our work. Please be clear about your requirements, as incomplete feature requests may simply be closed if we don't understand what you would like to see added to Chronograf.
|
||||
|
||||
Contributing to the source code
|
||||
-------------------------------
|
||||
## Contributing to the source code
|
||||
|
||||
Chronograf is built using Go for its API backend and serving the front-end assets, and uses Dep for dependency management. The front-end visualization is built with React (JavaScript) and uses Yarn for dependency management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
|
||||
|
||||
Submitting a pull request
|
||||
-------------------------
|
||||
To submit a pull request you should fork the Chronograf repository, and make your change on a feature branch of your fork. Then generate a pull request from your branch against *master* of the Chronograf repository. Include in your pull request details of your change -- the why *and* the how -- as well as the testing your performed. Also, be sure to run the test suite with your change in place. Changes that cause tests to fail cannot be merged.
|
||||
## Submitting a pull request
|
||||
|
||||
To submit a pull request you should fork the Chronograf repository, and make your change on a feature branch of your fork. Then generate a pull request from your branch against _master_ of the Chronograf repository. Include in your pull request details of your change -- the why _and_ the how -- as well as the testing your performed. Also, be sure to run the test suite with your change in place. Changes that cause tests to fail cannot be merged.
|
||||
|
||||
There will usually be some back and forth as we finalize the change, but once that completes it may be merged.
|
||||
|
||||
To assist in review for the PR, please add the following to your pull request comment:
|
||||
|
||||
```md
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Rebased/mergable
|
||||
- [ ] Tests pass
|
||||
- [ ] Sign [CLA](https://influxdata.com/community/cla/) (if not already signed)
|
||||
* [ ] CHANGELOG.md updated
|
||||
* [ ] Rebased/mergable
|
||||
* [ ] Tests pass
|
||||
* [ ] Sign [CLA](https://influxdata.com/community/cla/) (if not already signed)
|
||||
```
|
||||
|
||||
Signing the CLA
|
||||
---------------
|
||||
## Signing the CLA
|
||||
|
||||
If you are going to be contributing back to Chronograf please take a second to sign our CLA, which can be found
|
||||
[on our website](https://influxdata.com/community/cla/).
|
||||
|
||||
Installing & Using Yarn
|
||||
--------------
|
||||
## Installing & Using Yarn
|
||||
|
||||
You'll need to install Yarn to manage the frontend (JavaScript) dependencies.
|
||||
|
||||
* [Install Yarn](https://yarnpkg.com/en/docs/install)
|
||||
|
||||
To add a dependency via Yarn, for example, run `yarn add <dependency>` from within the `/chronograf/ui` directory.
|
||||
|
||||
Installing Go
|
||||
-------------
|
||||
Chronograf requires Go 1.7.1 or higher.
|
||||
## Installing Go
|
||||
|
||||
At InfluxData we find gvm, a Go version manager, useful for installing and managing Go. For instructions
|
||||
on how to install it see [the gvm page on github](https://github.com/moovweb/gvm).
|
||||
Chronograf requires Go 1.10 or higher.
|
||||
|
||||
After installing gvm you can install and set the default go version by
|
||||
running the following:
|
||||
## Installing & Using Dep
|
||||
|
||||
```bash
|
||||
gvm install go1.7.5
|
||||
gvm use go1.7.5 --default
|
||||
```
|
||||
|
||||
Installing & Using Dep
|
||||
--------------
|
||||
You'll need to install Dep to manage the backend (Go) dependencies.
|
||||
|
||||
* [Install Dep](https://github.com/golang/dep)
|
||||
|
||||
To add a dependency via Dep, for example, run `dep ensure -add <dependency>` from within the `/chronograf` directory. _Note that as of this writing, `dep ensure` will modify many extraneous vendor files, so you'll need to run `dep prune` to clean this up before committing your changes. Apparently, the next version of `dep` will take care of this step for you._
|
||||
|
||||
Revision Control Systems
|
||||
------------------------
|
||||
Go has the ability to import remote packages via revision control systems with the `go get` command. To ensure that you can retrieve any remote package, be sure to install the following rcs software to your system.
|
||||
Currently the project only depends on `git` and `mercurial`.
|
||||
## Revision Control Systems
|
||||
|
||||
Go has the ability to import remote packages via revision control systems with the `go get` command. To ensure that you can retrieve any remote package, be sure to install the following rcs software to your system.
|
||||
Currently the project only depends on `git`.
|
||||
|
||||
* [Install Git](http://git-scm.com/book/en/Getting-Started-Installing-Git)
|
||||
* [Install Mercurial](http://mercurial.selenic.com/wiki/Download)
|
||||
|
||||
Getting the source
|
||||
------------------
|
||||
## Getting the source
|
||||
|
||||
Setup the project structure and fetch the repo like so:
|
||||
|
||||
```bash
|
||||
|
@ -94,8 +82,8 @@ Setup the project structure and fetch the repo like so:
|
|||
|
||||
You can add the line `export GOPATH=$HOME/go` to your bash/zsh file to be set for every shell instead of having to manually run it everytime.
|
||||
|
||||
Cloning a fork
|
||||
--------------
|
||||
## Cloning a fork
|
||||
|
||||
If you wish to work with fork of Chronograf, your own fork for example, you must still follow the directory structure above. But instead of cloning the main repo, instead clone your fork. Follow the steps below to work with a fork:
|
||||
|
||||
```bash
|
||||
|
@ -107,8 +95,8 @@ If you wish to work with fork of Chronograf, your own fork for example, you must
|
|||
|
||||
Retaining the directory structure `$GOPATH/src/github.com/influxdata` is necessary so that Go imports work correctly.
|
||||
|
||||
Build and Test
|
||||
--------------
|
||||
## Build and Test
|
||||
|
||||
Make sure you have `go` and `yarn` installed and the project structure as shown above. We provide a `Makefile` to get up and running quickly, so all you'll need to do is run the following:
|
||||
|
||||
```bash
|
||||
|
@ -125,6 +113,6 @@ To run the tests, execute the following command:
|
|||
make test
|
||||
```
|
||||
|
||||
Continuous Integration testing
|
||||
-----
|
||||
## Continuous Integration testing
|
||||
|
||||
Chronograf uses CircleCI for continuous integration testing. To see how the code is built and tested, check out [this file](https://github.com/influxdata/chronograf/blob/master/Makefile). It closely follows the build and test process outlined above. You can see the exact version of Go Chronograf uses for testing by consulting that file.
|
||||
|
|
|
@ -11,6 +11,12 @@
|
|||
packages = ["."]
|
||||
revision = "3ec0642a7fb6488f65b06f9040adc67e3990296a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/boltdb/bolt"
|
||||
packages = ["."]
|
||||
|
@ -39,8 +45,38 @@
|
|||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["gogoproto","jsonpb","plugin/compare","plugin/defaultcheck","plugin/description","plugin/embedcheck","plugin/enumstringer","plugin/equal","plugin/face","plugin/gostring","plugin/marshalto","plugin/oneofcheck","plugin/populate","plugin/size","plugin/stringer","plugin/testgen","plugin/union","plugin/unmarshal","proto","protoc-gen-gogo","protoc-gen-gogo/descriptor","protoc-gen-gogo/generator","protoc-gen-gogo/grpc","protoc-gen-gogo/plugin","vanity","vanity/command"]
|
||||
revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b"
|
||||
packages = [
|
||||
"codec",
|
||||
"gogoproto",
|
||||
"jsonpb",
|
||||
"plugin/compare",
|
||||
"plugin/defaultcheck",
|
||||
"plugin/description",
|
||||
"plugin/embedcheck",
|
||||
"plugin/enumstringer",
|
||||
"plugin/equal",
|
||||
"plugin/face",
|
||||
"plugin/gostring",
|
||||
"plugin/marshalto",
|
||||
"plugin/oneofcheck",
|
||||
"plugin/populate",
|
||||
"plugin/size",
|
||||
"plugin/stringer",
|
||||
"plugin/testgen",
|
||||
"plugin/union",
|
||||
"plugin/unmarshal",
|
||||
"proto",
|
||||
"protoc-gen-gogo",
|
||||
"protoc-gen-gogo/descriptor",
|
||||
"protoc-gen-gogo/generator",
|
||||
"protoc-gen-gogo/grpc",
|
||||
"protoc-gen-gogo/plugin",
|
||||
"sortkeys",
|
||||
"types",
|
||||
"vanity",
|
||||
"vanity/command"
|
||||
]
|
||||
revision = "49944b4a4b085da44c43d4b233ea40787396371f"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
|
@ -50,7 +86,13 @@
|
|||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-cmp"
|
||||
packages = ["cmp","cmp/cmpopts","cmp/internal/diff","cmp/internal/function","cmp/internal/value"]
|
||||
packages = [
|
||||
"cmp",
|
||||
"cmp/cmpopts",
|
||||
"cmp/internal/diff",
|
||||
"cmp/internal/function",
|
||||
"cmp/internal/value"
|
||||
]
|
||||
revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97"
|
||||
version = "v0.1.0"
|
||||
|
||||
|
@ -65,22 +107,79 @@
|
|||
packages = ["query"]
|
||||
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/ifql"
|
||||
packages = [
|
||||
".",
|
||||
"ast",
|
||||
"compiler",
|
||||
"complete",
|
||||
"functions",
|
||||
"interpreter",
|
||||
"parser",
|
||||
"query",
|
||||
"query/control",
|
||||
"query/execute",
|
||||
"query/execute/storage",
|
||||
"query/plan",
|
||||
"semantic"
|
||||
]
|
||||
revision = "9445c4494d4421db2ab1aaaf6f4069446f11752e"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
packages = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"]
|
||||
packages = [
|
||||
"influxql",
|
||||
"influxql/internal",
|
||||
"influxql/neldermead",
|
||||
"models",
|
||||
"pkg/escape"
|
||||
]
|
||||
revision = "cd9363b52cac452113b95554d98a6be51beda24e"
|
||||
version = "v1.1.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
packages = ["client/v1","pipeline","pipeline/tick","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"]
|
||||
packages = [
|
||||
"client/v1",
|
||||
"pipeline",
|
||||
"pipeline/tick",
|
||||
"services/k8s/client",
|
||||
"tick",
|
||||
"tick/ast",
|
||||
"tick/stateful",
|
||||
"udf/agent"
|
||||
]
|
||||
revision = "6de30070b39afde111fea5e041281126fe8aae31"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/influxdata/tdigest"
|
||||
packages = ["."]
|
||||
revision = "617b83f940fd9acd207f712561a8a0590277fb38"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/usage-client"
|
||||
packages = ["v1"]
|
||||
revision = "6d3895376368aa52a3a81d2a16e90f0f52371967"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/influxdata/yamux"
|
||||
packages = ["."]
|
||||
revision = "1f58ded512de5feabbe30b60c7d33a7a896c5f16"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/influxdata/yarpc"
|
||||
packages = [
|
||||
".",
|
||||
"codes",
|
||||
"status",
|
||||
"yarpcproto"
|
||||
]
|
||||
revision = "f0da2db138cad2fb425541938fc28dd5a5bc6918"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jessevdk/go-flags"
|
||||
packages = ["."]
|
||||
|
@ -91,12 +190,60 @@
|
|||
packages = ["."]
|
||||
revision = "46eb4c183bfc1ebb527d9d19bcded39476302eb8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
packages = [
|
||||
".",
|
||||
"log"
|
||||
]
|
||||
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = ["prometheus"]
|
||||
revision = "c5b7fccd204277076155f10851dad72b76a49317"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
]
|
||||
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "780932d4fbbe0e69b84c34c20f5c8d0981e109ea"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
|
@ -115,12 +262,20 @@
|
|||
|
||||
[[projects]]
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","context/ctxhttp"]
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
]
|
||||
revision = "749a502dd1eaf3e5bfd4f8956748c502357c0bbe"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [".","github","heroku","internal"]
|
||||
packages = [
|
||||
".",
|
||||
"github",
|
||||
"heroku",
|
||||
"internal"
|
||||
]
|
||||
revision = "2f32c3ac0fa4fb807a0fcefb0b6f2468a0d99bd0"
|
||||
|
||||
[[projects]]
|
||||
|
@ -131,18 +286,31 @@
|
|||
|
||||
[[projects]]
|
||||
name = "google.golang.org/api"
|
||||
packages = ["gensupport","googleapi","googleapi/internal/uritemplates","oauth2/v2"]
|
||||
packages = [
|
||||
"gensupport",
|
||||
"googleapi",
|
||||
"googleapi/internal/uritemplates",
|
||||
"oauth2/v2"
|
||||
]
|
||||
revision = "bc20c61134e1d25265dd60049f5735381e79b631"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
packages = [
|
||||
"internal",
|
||||
"internal/base",
|
||||
"internal/datastore",
|
||||
"internal/log",
|
||||
"internal/remote_api",
|
||||
"internal/urlfetch",
|
||||
"urlfetch"
|
||||
]
|
||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "a4df1b0953349e64a89581f4b83ac3a2f40e17681e19f8de3cbf828b6375a3ba"
|
||||
inputs-digest = "3dee5534e81013d8f9d3b8cf80ad614dd8f0168114e63e1f1a5ba60733891e83"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -26,7 +26,7 @@ required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto",
|
|||
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b"
|
||||
revision = "49944b4a4b085da44c43d4b233ea40787396371f"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/google/go-github"
|
||||
|
@ -72,6 +72,9 @@ required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto",
|
|||
name = "github.com/influxdata/influxdb"
|
||||
version = "~1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/ifql"
|
||||
revision = "9445c4494d4421db2ab1aaaf6f4069446f11752e"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
|
|
3
Makefile
3
Makefile
|
@ -124,3 +124,6 @@ clean:
|
|||
|
||||
ctags:
|
||||
ctags -R --languages="Go" --exclude=.git --exclude=ui .
|
||||
|
||||
lint:
|
||||
cd ui && yarn prettier
|
||||
|
|
20
README.md
20
README.md
|
@ -136,7 +136,7 @@ option.
|
|||
## Versions
|
||||
|
||||
The most recent version of Chronograf is
|
||||
[v1.4.2.3](https://www.influxdata.com/downloads/).
|
||||
[v1.4.3.0](https://www.influxdata.com/downloads/).
|
||||
|
||||
Spotted a bug or have a feature request? Please open
|
||||
[an issue](https://github.com/influxdata/chronograf/issues/new)!
|
||||
|
@ -178,12 +178,12 @@ By default, chronograf runs on port `8888`.
|
|||
To get started right away with Docker, you can pull down our latest release:
|
||||
|
||||
```sh
|
||||
docker pull chronograf:1.4.2.3
|
||||
docker pull chronograf:1.4.3.0
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
* Chronograf works with go 1.8.x, node 6.x/7.x, and yarn 0.18+.
|
||||
* Chronograf works with go 1.10+, node 8.x, and yarn 1.5+.
|
||||
* Chronograf requires [Kapacitor](https://github.com/influxdata/kapacitor)
|
||||
1.2.x+ to create and store alerts.
|
||||
|
||||
|
@ -191,10 +191,16 @@ docker pull chronograf:1.4.2.3
|
|||
1. [Install Node and NPM](https://nodejs.org/en/download/)
|
||||
1. [Install yarn](https://yarnpkg.com/docs/install)
|
||||
1. [Setup your GOPATH](https://golang.org/doc/code.html#GOPATH)
|
||||
1. Run `go get github.com/influxdata/chronograf`
|
||||
1. Run `cd $GOPATH/src/github.com/influxdata/chronograf`
|
||||
1. Run `make`
|
||||
1. To install run `go install github.com/influxdata/chronograf/cmd/chronograf`
|
||||
1. Build the Chronograf package:
|
||||
```bash
|
||||
go get github.com/influxdata/chronograf
|
||||
cd $GOPATH/src/github.com/influxdata/chronograf
|
||||
make
|
||||
```
|
||||
1. Install the newly built Chronograf package:
|
||||
```bash
|
||||
go install github.com/influxdata/chronograf/cmd/chronograf
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"name": "CPU Usage",
|
||||
"queries": [
|
||||
{
|
||||
"query": "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"cpu\"",
|
||||
"query": "SELECT 100 - mean(\"usage_idle\") AS \"usage\" FROM \"cpu\"",
|
||||
"label": "% CPU time",
|
||||
"groupbys": [],
|
||||
"wheres": []
|
||||
|
|
|
@ -3,7 +3,7 @@ machine:
|
|||
services:
|
||||
- docker
|
||||
environment:
|
||||
DOCKER_TAG: chronograf-20180207
|
||||
DOCKER_TAG: chronograf-20180327
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
|
|
|
@ -20,24 +20,18 @@ RUN pip install boto requests python-jose --upgrade
|
|||
RUN gem install fpm
|
||||
|
||||
# Install node
|
||||
ENV NODE_VERSION v6.12.3
|
||||
RUN wget -q https://nodejs.org/dist/latest-v6.x/node-${NODE_VERSION}-linux-x64.tar.gz; \
|
||||
ENV NODE_VERSION v8.10.0
|
||||
RUN wget -q https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz; \
|
||||
tar -xvf node-${NODE_VERSION}-linux-x64.tar.gz -C / --strip-components=1; \
|
||||
rm -f node-${NODE_VERSION}-linux-x64.tar.gz
|
||||
|
||||
# Update npm
|
||||
RUN cd $(npm root -g)/npm \
|
||||
&& npm install fs-extra \
|
||||
&& sed -i -e s/graceful-fs/fs-extra/ -e s/fs.rename/fs.move/ ./lib/utils/rename.js
|
||||
RUN npm install npm -g
|
||||
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && \
|
||||
apt-get update && sudo apt-get install yarn
|
||||
|
||||
# Install go
|
||||
ENV GOPATH /root/go
|
||||
ENV GO_VERSION 1.9.4
|
||||
ENV GO_VERSION 1.10
|
||||
ENV GO_ARCH amd64
|
||||
RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \
|
||||
tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz ; \
|
||||
|
|
|
@ -2715,6 +2715,11 @@ func TestServer(t *testing.T) {
|
|||
"logout": "/oauth/logout",
|
||||
"external": {
|
||||
"statusFeed": ""
|
||||
},
|
||||
"ifql": {
|
||||
"ast": "/chronograf/v1/ifql/ast",
|
||||
"self": "/chronograf/v1/ifql",
|
||||
"suggestions": "/chronograf/v1/ifql/suggestions"
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
@ -2798,6 +2803,11 @@ func TestServer(t *testing.T) {
|
|||
"logout": "/oauth/logout",
|
||||
"external": {
|
||||
"statusFeed": ""
|
||||
},
|
||||
"ifql": {
|
||||
"ast": "/chronograf/v1/ifql/ast",
|
||||
"self": "/chronograf/v1/ifql",
|
||||
"suggestions": "/chronograf/v1/ifql/suggestions"
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/ifql"
|
||||
"github.com/influxdata/ifql/parser"
|
||||
)
|
||||
|
||||
// SuggestionsResponse provides a list of available IFQL functions
|
||||
type SuggestionsResponse struct {
|
||||
Functions []SuggestionResponse `json:"funcs"`
|
||||
}
|
||||
|
||||
// SuggestionResponse provides the parameters available for a given IFQL function
|
||||
type SuggestionResponse struct {
|
||||
Name string `json:"name"`
|
||||
Params map[string]string `json:"params"`
|
||||
}
|
||||
|
||||
type ifqlLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
Suggestions string `json:"suggestions"` // URL for ifql builder function suggestions
|
||||
}
|
||||
|
||||
type ifqlResponse struct {
|
||||
Links ifqlLinks `json:"links"`
|
||||
}
|
||||
|
||||
// IFQL returns a list of links for the IFQL API
|
||||
func (s *Service) IFQL(w http.ResponseWriter, r *http.Request) {
|
||||
httpAPIIFQL := "/chronograf/v1/ifql"
|
||||
res := ifqlResponse{
|
||||
Links: ifqlLinks{
|
||||
Self: fmt.Sprintf("%s", httpAPIIFQL),
|
||||
Suggestions: fmt.Sprintf("%s/suggestions", httpAPIIFQL),
|
||||
},
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// IFQLSuggestions returns a list of available IFQL functions for the IFQL Builder
|
||||
func (s *Service) IFQLSuggestions(w http.ResponseWriter, r *http.Request) {
|
||||
completer := ifql.DefaultCompleter()
|
||||
names := completer.FunctionNames()
|
||||
var functions []SuggestionResponse
|
||||
for _, name := range names {
|
||||
suggestion, err := completer.FunctionSuggestion(name)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
functions = append(functions, SuggestionResponse{
|
||||
Name: name,
|
||||
Params: suggestion.Params,
|
||||
})
|
||||
}
|
||||
res := SuggestionsResponse{Functions: functions}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// IFQLSuggestion returns the function parameters for the requested function
|
||||
func (s *Service) IFQLSuggestion(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
name := httprouter.GetParamFromContext(ctx, "name")
|
||||
completer := ifql.DefaultCompleter()
|
||||
|
||||
suggestion, err := completer.FunctionSuggestion(name)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, SuggestionResponse{Name: name, Params: suggestion.Params}, s.Logger)
|
||||
}
|
||||
|
||||
type ASTRequest struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func (s *Service) IFQLAST(w http.ResponseWriter, r *http.Request) {
|
||||
var request ASTRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
}
|
||||
|
||||
ast, err := parser.NewAST(request.Body)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, ast, s.Logger)
|
||||
}
|
|
@ -5,6 +5,12 @@ import (
|
|||
"net/url"
|
||||
)
|
||||
|
||||
type getIFQLLinksResponse struct {
|
||||
AST string `json:"ast"`
|
||||
Self string `json:"self"`
|
||||
Suggestions string `json:"suggestions"`
|
||||
}
|
||||
|
||||
type getConfigLinksResponse struct {
|
||||
Self string `json:"self"` // Location of the whole global application configuration
|
||||
Auth string `json:"auth"` // Location of the auth section of the global application configuration
|
||||
|
|
|
@ -156,6 +156,12 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.PATCH("/chronograf/v1/sources/:id", EnsureEditor(service.UpdateSource))
|
||||
router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource))
|
||||
|
||||
// IFQL
|
||||
router.GET("/chronograf/v1/ifql", EnsureViewer(service.IFQL))
|
||||
router.POST("/chronograf/v1/ifql/ast", EnsureViewer(service.IFQLAST))
|
||||
router.GET("/chronograf/v1/ifql/suggestions", EnsureViewer(service.IFQLSuggestions))
|
||||
router.GET("/chronograf/v1/ifql/suggestions/:name", EnsureViewer(service.IFQLSuggestion))
|
||||
|
||||
// Source Proxy to Influx; Has gzip compression around the handler
|
||||
influx := gziphandler.GzipHandler(http.HandlerFunc(EnsureViewer(service.Influx)))
|
||||
router.Handler("POST", "/chronograf/v1/sources/:id/proxy", influx)
|
||||
|
|
|
@ -44,6 +44,7 @@ type getRoutesResponse struct {
|
|||
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
|
||||
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
|
||||
ExternalLinks getExternalLinksResponse `json:"external"` // All external links for the client to use
|
||||
IFQL getIFQLLinksResponse `json:"ifql"`
|
||||
}
|
||||
|
||||
// AllRoutes is a handler that returns all links to resources in Chronograf server, as well as
|
||||
|
@ -94,6 +95,11 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
StatusFeed: &a.StatusFeed,
|
||||
CustomLinks: customLinks,
|
||||
},
|
||||
IFQL: getIFQLLinksResponse{
|
||||
Self: "/chronograf/v1/ifql",
|
||||
AST: "/chronograf/v1/ifql/ast",
|
||||
Suggestions: "/chronograf/v1/ifql/suggestions",
|
||||
},
|
||||
}
|
||||
|
||||
// The JSON response will have no field present for the LogoutLink if there is no logout link.
|
||||
|
|
|
@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutes not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -67,7 +67,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -100,7 +100,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
|
1062
server/swagger.json
1062
server/swagger.json
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +0,0 @@
|
|||
import { configure } from '@kadira/storybook';
|
||||
|
||||
function loadStories() {
|
||||
require('../stories');
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
|
@ -1 +0,0 @@
|
|||
<link href="/style.css" rel="stylesheet">
|
|
@ -1,54 +0,0 @@
|
|||
const path = require('path');
|
||||
var ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||
|
||||
// you can use this file to add your custom webpack plugins, loaders and anything you like.
|
||||
// This is just the basic way to add addional webpack configurations.
|
||||
// For more information refer the docs: https://getstorybook.io/docs/configurations/custom-webpack-config
|
||||
|
||||
// IMPORTANT
|
||||
// When you add this file, we won't add the default configurations which is similar
|
||||
// to "React Create App". This only has babel loader to load JavaScript.
|
||||
|
||||
module.exports = {
|
||||
debug: true,
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new ExtractTextPlugin("style.css"),
|
||||
],
|
||||
output: {
|
||||
publicPath: '/',
|
||||
path: path.resolve(__dirname, '../build'),
|
||||
filename: '[name].[chunkhash].dev.js',
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.scss$/,
|
||||
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader!resolve-url!sass?sourceMap'),
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'),
|
||||
},
|
||||
{
|
||||
test : /\.(ico|png|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||
options: {
|
||||
limit: 100000,
|
||||
},
|
||||
loader : 'file-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
src: path.resolve(__dirname, '..', 'src'),
|
||||
shared: path.resolve(__dirname, '..', 'src', 'shared'),
|
||||
style: path.resolve(__dirname, '..', 'src', 'style'),
|
||||
utils: path.resolve(__dirname, '..', 'src', 'utils'),
|
||||
},
|
||||
},
|
||||
sassLoader: {
|
||||
includePaths: [path.resolve(__dirname, "node_modules")],
|
||||
},
|
||||
postcss: require('../webpack/postcss'),
|
||||
};
|
|
@ -8,7 +8,7 @@ module.exports = {
|
|||
],
|
||||
modulePaths: ['<rootDir>', '<rootDir>/node_modules/'],
|
||||
moduleDirectories: ['src'],
|
||||
setupFiles: ['<rootDir>/test/setupTests.js'],
|
||||
setupFiles: ['<rootDir>/test/setup.js'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
const webpack = require('webpack')
|
||||
const path = require('path')
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
browsers: ['PhantomJS'],
|
||||
frameworks: ['mocha'],
|
||||
files: [
|
||||
'node_modules/babel-polyfill/dist/polyfill.js',
|
||||
'spec/spec-helper.js',
|
||||
'spec/index.js',
|
||||
],
|
||||
preprocessors: {
|
||||
'spec/spec-helper.js': ['webpack', 'sourcemap'],
|
||||
'spec/index.js': ['webpack', 'sourcemap'],
|
||||
},
|
||||
// For more detailed reporting on tests, you can add 'verbose' and/or 'progress'.
|
||||
// This can also be done via the command line with `yarn test -- --reporters=verbose`.
|
||||
reporters: ['dots'],
|
||||
webpack: {
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
{
|
||||
test: /\.css/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'style-loader!css-loader!postcss-loader',
|
||||
},
|
||||
{
|
||||
test: /\.scss/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'style-loader!css-loader!sass-loader',
|
||||
},
|
||||
{
|
||||
// Sinon behaves weirdly with webpack, see https://github.com/webpack/webpack/issues/304
|
||||
test: /sinon\/pkg\/sinon\.js/,
|
||||
loader: 'imports?define=>false,require=>false',
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
'react/addons': true,
|
||||
'react/lib/ExecutionEnvironment': true,
|
||||
'react/lib/ReactContext': true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
app: path.resolve(__dirname, 'app'),
|
||||
src: path.resolve(__dirname, 'src'),
|
||||
chronograf: path.resolve(__dirname, 'src', 'chronograf'),
|
||||
shared: path.resolve(__dirname, 'src', 'shared'),
|
||||
style: path.resolve(__dirname, 'src', 'style'),
|
||||
utils: path.resolve(__dirname, 'src', 'utils'),
|
||||
},
|
||||
},
|
||||
},
|
||||
webpackServer: {
|
||||
noInfo: true, // please don't spam the console when running in karma!
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
jest.mock('src/utils/ajax', () => require('mocks/utils/ajax'))
|
||||
|
||||
export const getSuggestions = jest.fn(() => Promise.resolve([]))
|
||||
export const getAST = jest.fn(() => Promise.resolve({}))
|
|
@ -1 +1 @@
|
|||
export default jest.fn(() => Promise.resolve())
|
||||
export default jest.fn(() => Promise.resolve({data: {}}))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.4.2-3",
|
||||
"version": "1.4.3-0",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, {Component} from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import ConfirmOrCancel from 'shared/components/ConfirmOrCancel'
|
||||
|
||||
class ChangePassRow extends Component {
|
||||
constructor(props) {
|
||||
|
@ -59,7 +59,7 @@ class ChangePassRow extends Component {
|
|||
onKeyPress={this.handleKeyPress(user)}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<ConfirmButtons
|
||||
<ConfirmOrCancel
|
||||
onConfirm={this.handleSubmit}
|
||||
item={user}
|
||||
onCancel={this.handleCancel}
|
||||
|
|
|
@ -8,7 +8,7 @@ import onClickOutside from 'react-onclickoutside'
|
|||
import {notify as notifyAction} from 'shared/actions/notifications'
|
||||
|
||||
import {formatRPDuration} from 'utils/formatting'
|
||||
import YesNoButtons from 'shared/components/YesNoButtons'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
import {DATABASE_TABLE} from 'src/admin/constants/tableSizing'
|
||||
import {notifyRetentionPolicyCantHaveEmptyFields} from 'shared/copy/notifications'
|
||||
|
||||
|
@ -17,7 +17,6 @@ class DatabaseRow extends Component {
|
|||
super(props)
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
isDeleting: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +38,6 @@ class DatabaseRow extends Component {
|
|||
}
|
||||
|
||||
this.handleEndEdit()
|
||||
this.handleEndDelete()
|
||||
}
|
||||
|
||||
handleStartEdit = () => {
|
||||
|
@ -50,14 +48,6 @@ class DatabaseRow extends Component {
|
|||
this.setState({isEditing: false})
|
||||
}
|
||||
|
||||
handleStartDelete = () => {
|
||||
this.setState({isDeleting: true})
|
||||
}
|
||||
|
||||
handleEndDelete = () => {
|
||||
this.setState({isDeleting: false})
|
||||
}
|
||||
|
||||
handleCreate = () => {
|
||||
const {database, retentionPolicy, onCreate} = this.props
|
||||
const validInputs = this.getInputValues()
|
||||
|
@ -140,7 +130,7 @@ class DatabaseRow extends Component {
|
|||
isDeletable,
|
||||
isRFDisplayed,
|
||||
} = this.props
|
||||
const {isEditing, isDeleting} = this.state
|
||||
const {isEditing} = this.state
|
||||
|
||||
const formattedDuration = formatRPDuration(duration)
|
||||
|
||||
|
@ -198,11 +188,18 @@ class DatabaseRow extends Component {
|
|||
className="text-right"
|
||||
style={{width: `${DATABASE_TABLE.colDelete}px`}}
|
||||
>
|
||||
<YesNoButtons
|
||||
buttonSize="btn-xs"
|
||||
onConfirm={isNew ? this.handleCreate : this.handleUpdate}
|
||||
onCancel={isNew ? this.handleRemove : this.handleEndEdit}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-xs btn-info"
|
||||
onClick={isNew ? this.handleRemove : this.handleEndEdit}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-success"
|
||||
onClick={isNew ? this.handleCreate : this.handleUpdate}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
@ -216,17 +213,11 @@ class DatabaseRow extends Component {
|
|||
<span className="default-source-label">default</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td
|
||||
onClick={this.handleStartEdit}
|
||||
style={{width: `${DATABASE_TABLE.colDuration}px`}}
|
||||
>
|
||||
<td style={{width: `${DATABASE_TABLE.colDuration}px`}}>
|
||||
{formattedDuration}
|
||||
</td>
|
||||
{isRFDisplayed ? (
|
||||
<td
|
||||
onClick={this.handleStartEdit}
|
||||
style={{width: `${DATABASE_TABLE.colReplication}px`}}
|
||||
>
|
||||
<td style={{width: `${DATABASE_TABLE.colReplication}px`}}>
|
||||
{replication}
|
||||
</td>
|
||||
) : null}
|
||||
|
@ -234,21 +225,20 @@ class DatabaseRow extends Component {
|
|||
className="text-right"
|
||||
style={{width: `${DATABASE_TABLE.colDelete}px`}}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<YesNoButtons
|
||||
onConfirm={onDelete(database, retentionPolicy)}
|
||||
onCancel={this.handleEndDelete}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-danger btn-xs table--show-on-row-hover"
|
||||
style={isDeletable ? {} : {visibility: 'hidden'}}
|
||||
onClick={this.handleStartDelete}
|
||||
>
|
||||
{`Delete ${name}`}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-xs btn-info table--show-on-row-hover"
|
||||
onClick={this.handleStartEdit}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<ConfirmButton
|
||||
text={`Delete ${name}`}
|
||||
confirmAction={onDelete(database, retentionPolicy)}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
customClass="table--show-on-row-hover"
|
||||
disabled={!isDeletable}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import {connect} from 'react-redux'
|
|||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import {notify as notifyAction} from 'shared/actions/notifications'
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import ConfirmOrCancel from 'shared/components/ConfirmOrCancel'
|
||||
import {notifyDatabaseDeleteConfirmationRequired} from 'shared/copy/notifications'
|
||||
|
||||
const DatabaseTableHeader = ({
|
||||
|
@ -101,7 +101,7 @@ const Header = ({
|
|||
autoComplete={false}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<ConfirmButtons
|
||||
<ConfirmOrCancel
|
||||
item={database}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
|
@ -132,7 +132,11 @@ const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) => (
|
|||
spellCheck={false}
|
||||
autoComplete={false}
|
||||
/>
|
||||
<ConfirmButtons item={database} onConfirm={onConfirm} onCancel={onCancel} />
|
||||
<ConfirmOrCancel
|
||||
item={database}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
|
|
@ -1,72 +1,43 @@
|
|||
import React, {Component} from 'react'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
import {QUERIES_TABLE} from 'src/admin/constants/tableSizing'
|
||||
|
||||
class QueryRow extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
confirmingKill: false,
|
||||
}
|
||||
const QueryRow = ({query, onKill}) => {
|
||||
const {database, duration} = query
|
||||
const wrappedKill = () => {
|
||||
onKill(query.id)
|
||||
}
|
||||
|
||||
handleInitiateKill = () => {
|
||||
this.setState({confirmingKill: true})
|
||||
}
|
||||
|
||||
handleFinishHim = () => {
|
||||
this.props.onKill(this.props.query.id)
|
||||
}
|
||||
|
||||
handleShowMercy = () => {
|
||||
this.setState({confirmingKill: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {query: {database, query, duration}} = this.props
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td
|
||||
style={{width: `${QUERIES_TABLE.colDatabase}px`}}
|
||||
className="monotype"
|
||||
>
|
||||
{database}
|
||||
</td>
|
||||
<td>
|
||||
<code>{query}</code>
|
||||
</td>
|
||||
<td
|
||||
style={{width: `${QUERIES_TABLE.colRunning}px`}}
|
||||
className="monotype"
|
||||
>
|
||||
{duration}
|
||||
</td>
|
||||
<td
|
||||
style={{width: `${QUERIES_TABLE.colKillQuery}px`}}
|
||||
className="text-right"
|
||||
>
|
||||
{this.state.confirmingKill ? (
|
||||
<ConfirmButtons
|
||||
onConfirm={this.handleFinishHim}
|
||||
onCancel={this.handleShowMercy}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-xs btn-danger table--show-on-row-hover"
|
||||
onClick={this.handleInitiateKill}
|
||||
>
|
||||
Kill
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td
|
||||
style={{width: `${QUERIES_TABLE.colDatabase}px`}}
|
||||
className="monotype"
|
||||
>
|
||||
{database}
|
||||
</td>
|
||||
<td>
|
||||
<code>{query.query}</code>
|
||||
</td>
|
||||
<td style={{width: `${QUERIES_TABLE.colRunning}px`}} className="monotype">
|
||||
{duration}
|
||||
</td>
|
||||
<td
|
||||
style={{width: `${QUERIES_TABLE.colKillQuery}px`}}
|
||||
className="text-right"
|
||||
>
|
||||
<ConfirmButton
|
||||
text="Kill"
|
||||
confirmAction={wrappedKill}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const {func, shape} = PropTypes
|
||||
|
|
|
@ -6,8 +6,8 @@ import classnames from 'classnames'
|
|||
|
||||
import RoleEditingRow from 'src/admin/components/RoleEditingRow'
|
||||
import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
|
||||
import ConfirmOrCancel from 'shared/components/ConfirmOrCancel'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
import {ROLES_TABLE} from 'src/admin/constants/tableSizing'
|
||||
|
||||
const RoleRow = ({
|
||||
|
@ -51,7 +51,7 @@ const RoleRow = ({
|
|||
className="text-right"
|
||||
style={{width: `${ROLES_TABLE.colDelete}px`}}
|
||||
>
|
||||
<ConfirmButtons
|
||||
<ConfirmOrCancel
|
||||
item={role}
|
||||
onConfirm={onSave}
|
||||
onCancel={onCancel}
|
||||
|
@ -62,6 +62,10 @@ const RoleRow = ({
|
|||
)
|
||||
}
|
||||
|
||||
const wrappedDelete = () => {
|
||||
onDelete(role)
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td style={{width: `${ROLES_TABLE.colName}px`}}>{roleName}</td>
|
||||
|
@ -97,11 +101,15 @@ const RoleRow = ({
|
|||
/>
|
||||
) : null}
|
||||
</td>
|
||||
<DeleteConfirmTableCell
|
||||
onDelete={onDelete}
|
||||
item={role}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
<td className="text-right">
|
||||
<ConfirmButton
|
||||
customClass="table--show-on-row-hover"
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Delete Role"
|
||||
confirmAction={wrappedDelete}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import classnames from 'classnames'
|
|||
import UserEditName from 'src/admin/components/UserEditName'
|
||||
import UserNewPassword from 'src/admin/components/UserNewPassword'
|
||||
import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
|
||||
import ConfirmOrCancel from 'shared/components/ConfirmOrCancel'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
import ChangePassRow from 'src/admin/components/ChangePassRow'
|
||||
import {USERS_TABLE} from 'src/admin/constants/tableSizing'
|
||||
|
||||
|
@ -46,6 +46,10 @@ const UserRow = ({
|
|||
|
||||
const perms = _.get(permissions, ['0', 'allowed'], [])
|
||||
|
||||
const wrappedDelete = () => {
|
||||
onDelete(user)
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<tr className="admin-table--edit-row">
|
||||
|
@ -62,7 +66,7 @@ const UserRow = ({
|
|||
className="text-right"
|
||||
style={{width: `${USERS_TABLE.colDelete}px`}}
|
||||
>
|
||||
<ConfirmButtons
|
||||
<ConfirmOrCancel
|
||||
item={user}
|
||||
onConfirm={onSave}
|
||||
onCancel={onCancel}
|
||||
|
@ -118,11 +122,15 @@ const UserRow = ({
|
|||
/>
|
||||
) : null}
|
||||
</td>
|
||||
<DeleteConfirmTableCell
|
||||
onDelete={onDelete}
|
||||
item={user}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
<td className="text-right" style={{width: `${USERS_TABLE.colDelete}px`}}>
|
||||
<ConfirmButton
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Delete User"
|
||||
confirmAction={wrappedDelete}
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Tags from 'shared/components/Tags'
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
|
||||
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
const {
|
||||
colOrganizations,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colSuperAdmin,
|
||||
colActions,
|
||||
} = ALL_USERS_TABLE
|
||||
|
||||
const AllUsersTableRow = ({
|
||||
organizations,
|
||||
user,
|
||||
onAddToOrganization,
|
||||
onRemoveFromOrganization,
|
||||
onChangeSuperAdmin,
|
||||
onDelete,
|
||||
meID,
|
||||
}) => {
|
||||
const dropdownOrganizationsItems = organizations
|
||||
.filter(o => !user.roles.find(role => role.organization === o.id))
|
||||
.map(o => ({
|
||||
...o,
|
||||
text: o.name,
|
||||
}))
|
||||
|
||||
const userIsMe = user.id === meID
|
||||
|
||||
const userOrganizations = user.roles.map(r => ({
|
||||
...r,
|
||||
name: organizations.find(o => r.organization === o.id).name,
|
||||
}))
|
||||
|
||||
const wrappedDelete = () => {
|
||||
onDelete(user)
|
||||
}
|
||||
|
||||
const removeWarning = userIsMe
|
||||
? 'Delete your user record\nand log yourself out?'
|
||||
: 'Delete this user?'
|
||||
|
||||
return (
|
||||
<tr className={'chronograf-admin-table--user'}>
|
||||
<td>
|
||||
{userIsMe ? (
|
||||
<strong className="chronograf-user--me">
|
||||
<span className="icon user" />
|
||||
{user.name}
|
||||
</strong>
|
||||
) : (
|
||||
<strong>{user.name}</strong>
|
||||
)}
|
||||
</td>
|
||||
<td style={{width: colOrganizations}}>
|
||||
<Tags
|
||||
tags={userOrganizations}
|
||||
onDeleteTag={onRemoveFromOrganization(user)}
|
||||
emptyStateText="None"
|
||||
addMenuItems={dropdownOrganizationsItems}
|
||||
addMenuChoose={onAddToOrganization(user)}
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colProvider}}>{user.provider}</td>
|
||||
<td style={{width: colScheme}}>{user.scheme}</td>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
<SlideToggle
|
||||
active={user.superAdmin}
|
||||
onToggle={onChangeSuperAdmin(user)}
|
||||
size="xs"
|
||||
disabled={userIsMe}
|
||||
/>
|
||||
</td>
|
||||
<td style={{textAlign: 'right', width: colActions}}>
|
||||
<ConfirmButton
|
||||
confirmText={removeWarning}
|
||||
confirmAction={wrappedDelete}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Delete"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
AllUsersTableRow.propTypes = {
|
||||
user: shape(),
|
||||
organization: shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
onAddToOrganization: func.isRequired,
|
||||
onRemoveFromOrganization: func.isRequired,
|
||||
onChangeSuperAdmin: func.isRequired,
|
||||
onDelete: func.isRequired,
|
||||
meID: string.isRequired,
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
export default AllUsersTableRow
|
|
@ -0,0 +1,158 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Tags from 'src/shared/components/Tags'
|
||||
import SlideToggle from 'src/shared/components/SlideToggle'
|
||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
|
||||
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
|
||||
const {
|
||||
colOrganizations,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colSuperAdmin,
|
||||
colActions,
|
||||
} = ALL_USERS_TABLE
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Role {
|
||||
organization: string
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
roles: Role[]
|
||||
superAdmin: boolean
|
||||
scheme: string
|
||||
provider: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
user: User
|
||||
organization: Organization
|
||||
onAddToOrganization: (user: User) => () => void
|
||||
onRemoveFromOrganization: (user: User) => () => void
|
||||
onChangeSuperAdmin: (user: User) => () => void
|
||||
onDelete: (user: User) => void
|
||||
meID: string
|
||||
organizations: Organization[]
|
||||
}
|
||||
|
||||
export default class AllUsersTableRow extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
user,
|
||||
onRemoveFromOrganization,
|
||||
onAddToOrganization,
|
||||
onChangeSuperAdmin,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<tr className={'chronograf-admin-table--user'}>
|
||||
{this.userNameTableCell}
|
||||
<td style={{width: colOrganizations}}>
|
||||
<Tags
|
||||
tags={this.userOrganizationTags}
|
||||
onDeleteTag={onRemoveFromOrganization(user)}
|
||||
addMenuItems={this.dropdownOrganizationsItems}
|
||||
addMenuChoose={onAddToOrganization(user)}
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colProvider}}>{user.provider}</td>
|
||||
<td style={{width: colScheme}}>{user.scheme}</td>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
<SlideToggle
|
||||
active={user.superAdmin}
|
||||
onToggle={onChangeSuperAdmin(user)}
|
||||
size="xs"
|
||||
disabled={this.userIsMe}
|
||||
/>
|
||||
</td>
|
||||
<td style={{textAlign: 'right', width: colActions}}>
|
||||
<ConfirmButton
|
||||
confirmText={this.removeWarning}
|
||||
confirmAction={this.handleDelete}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Delete"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
private get userNameTableCell() {
|
||||
const {user} = this.props
|
||||
|
||||
return (
|
||||
<td>
|
||||
{this.userIsMe ? (
|
||||
<strong className="chronograf-user--me">
|
||||
<span className="icon user" />
|
||||
{user.name}
|
||||
</strong>
|
||||
) : (
|
||||
<strong>{user.name}</strong>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
private get userOrganizationTags() {
|
||||
return this.userRoles.map((role: Role) => ({
|
||||
...role,
|
||||
name: this.findOrganizationByRole(role).name,
|
||||
}))
|
||||
}
|
||||
|
||||
private findOrganizationByRole(role: Role): Organization | null {
|
||||
const {organizations} = this.props
|
||||
|
||||
return _.find(organizations, org => role.organization === org.id)
|
||||
}
|
||||
|
||||
private get removeWarning() {
|
||||
if (this.userIsMe) {
|
||||
return 'Delete your user record\nand log yourself out?'
|
||||
}
|
||||
|
||||
return 'Delete this user?'
|
||||
}
|
||||
|
||||
private get userIsMe() {
|
||||
const {user, meID} = this.props
|
||||
return user.id === meID
|
||||
}
|
||||
|
||||
private get dropdownOrganizationsItems() {
|
||||
return this.userOrganizations.map(o => ({...o, text: o.name}))
|
||||
}
|
||||
|
||||
private get userOrganizations() {
|
||||
const {organizations} = this.props
|
||||
return _.filter(organizations, _.negate(this.isUserOrganization))
|
||||
}
|
||||
|
||||
private get userRoles(): Role[] {
|
||||
return _.get(this.props.user, 'roles', [])
|
||||
}
|
||||
|
||||
private isUserOrganization = (organization): boolean => {
|
||||
return !!_.find(
|
||||
this.userRoles,
|
||||
role => role.organization === organization.id
|
||||
)
|
||||
}
|
||||
|
||||
private handleDelete = () => {
|
||||
const {onDelete, user} = this.props
|
||||
onDelete(user)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import {connect} from 'react-redux'
|
|||
import {bindActionCreators} from 'redux'
|
||||
import {withRouter} from 'react-router'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import InputClickToEdit from 'shared/components/InputClickToEdit'
|
||||
|
||||
|
@ -13,32 +13,7 @@ import {meChangeOrganizationAsync} from 'shared/actions/auth'
|
|||
import {DEFAULT_ORG_ID} from 'src/admin/constants/chronografAdmin'
|
||||
import {USER_ROLES} from 'src/admin/constants/chronografAdmin'
|
||||
|
||||
const OrganizationsTableRowDeleteButton = ({organization, onClickDelete}) =>
|
||||
organization.id === DEFAULT_ORG_ID ? (
|
||||
<button
|
||||
className="btn btn-sm btn-default btn-square orgs-table--delete"
|
||||
disabled={true}
|
||||
>
|
||||
<span className="icon trash" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-sm btn-default btn-square"
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
<span className="icon trash" />
|
||||
</button>
|
||||
)
|
||||
|
||||
class OrganizationsTableRow extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isDeleting: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeCurrentOrganization = async () => {
|
||||
const {router, links, meChangeOrganization, organization} = this.props
|
||||
|
||||
|
@ -49,13 +24,6 @@ class OrganizationsTableRow extends Component {
|
|||
const {organization, onRename} = this.props
|
||||
onRename(organization, newName)
|
||||
}
|
||||
handleDeleteClick = () => {
|
||||
this.setState({isDeleting: true})
|
||||
}
|
||||
|
||||
handleDismissDeleteConfirmation = () => {
|
||||
this.setState({isDeleting: false})
|
||||
}
|
||||
|
||||
handleDeleteOrg = organization => {
|
||||
const {onDelete} = this.props
|
||||
|
@ -68,7 +36,6 @@ class OrganizationsTableRow extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {isDeleting} = this.state
|
||||
const {organization, currentOrganization} = this.props
|
||||
|
||||
const dropdownRolesItems = USER_ROLES.map(role => ({
|
||||
|
@ -76,10 +43,6 @@ class OrganizationsTableRow extends Component {
|
|||
text: role.name,
|
||||
}))
|
||||
|
||||
const defaultRoleClassName = isDeleting
|
||||
? 'fancytable--td orgs-table--default-role deleting'
|
||||
: 'fancytable--td orgs-table--default-role'
|
||||
|
||||
return (
|
||||
<div className="fancytable--row">
|
||||
<div className="fancytable--td orgs-table--active">
|
||||
|
@ -101,7 +64,7 @@ class OrganizationsTableRow extends Component {
|
|||
wrapperClass="fancytable--td orgs-table--name"
|
||||
onBlur={this.handleUpdateOrgName}
|
||||
/>
|
||||
<div className={defaultRoleClassName}>
|
||||
<div className="fancytable--td orgs-table--default-role">
|
||||
<Dropdown
|
||||
items={dropdownRolesItems}
|
||||
onChoose={this.handleChooseDefaultRole}
|
||||
|
@ -109,21 +72,14 @@ class OrganizationsTableRow extends Component {
|
|||
className="dropdown-stretch"
|
||||
/>
|
||||
</div>
|
||||
{isDeleting ? (
|
||||
<ConfirmButtons
|
||||
item={organization}
|
||||
onCancel={this.handleDismissDeleteConfirmation}
|
||||
onConfirm={this.handleDeleteOrg}
|
||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||
confirmLeft={true}
|
||||
confirmTitle="Delete"
|
||||
/>
|
||||
) : (
|
||||
<OrganizationsTableRowDeleteButton
|
||||
organization={organization}
|
||||
onClickDelete={this.handleDeleteClick}
|
||||
/>
|
||||
)}
|
||||
<ConfirmButton
|
||||
confirmAction={this.handleDeleteOrg}
|
||||
confirmText="Delete Organization?"
|
||||
size="btn-sm"
|
||||
square={true}
|
||||
icon="trash"
|
||||
disabled={organization.id === DEFAULT_ORG_ID}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -161,15 +117,6 @@ OrganizationsTableRow.propTypes = {
|
|||
meChangeOrganization: func.isRequired,
|
||||
}
|
||||
|
||||
OrganizationsTableRowDeleteButton.propTypes = {
|
||||
organization: shape({
|
||||
id: string, // when optimistically created, organization will not have an id
|
||||
name: string.isRequired,
|
||||
defaultRole: string.isRequired,
|
||||
}).isRequired,
|
||||
onClickDelete: func.isRequired,
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
meChangeOrganization: bindActionCreators(meChangeOrganizationAsync, dispatch),
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import ConfirmOrCancel from 'shared/components/ConfirmOrCancel'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
import {USER_ROLES} from 'src/admin/constants/chronografAdmin'
|
||||
|
@ -74,7 +74,7 @@ class OrganizationsTableRowNew extends Component {
|
|||
ref={r => (this.inputRef = r)}
|
||||
/>
|
||||
</div>
|
||||
<div className="fancytable--td orgs-table--default-role deleting">
|
||||
<div className="fancytable--td orgs-table--default-role creating">
|
||||
<Dropdown
|
||||
items={dropdownRolesItems}
|
||||
onChoose={this.handleChooseDefaultRole}
|
||||
|
@ -82,7 +82,7 @@ class OrganizationsTableRowNew extends Component {
|
|||
className="dropdown-stretch"
|
||||
/>
|
||||
</div>
|
||||
<ConfirmButtons
|
||||
<ConfirmOrCancel
|
||||
disabled={isSaveDisabled}
|
||||
onCancel={onCancelCreateOrganization}
|
||||
onConfirm={this.handleClickSave}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import InputClickToEdit from 'shared/components/InputClickToEdit'
|
||||
|
||||
|
@ -13,21 +13,11 @@ class ProvidersTableRow extends Component {
|
|||
|
||||
this.state = {
|
||||
...this.props.mapping,
|
||||
isDeleting: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.setState({isDeleting: true})
|
||||
}
|
||||
|
||||
handleDismissDeleteConfirmation = () => {
|
||||
this.setState({isDeleting: false})
|
||||
}
|
||||
|
||||
handleDeleteMap = mapping => {
|
||||
const {onDelete} = this.props
|
||||
this.setState({isDeleting: false})
|
||||
onDelete(mapping)
|
||||
}
|
||||
|
||||
|
@ -106,22 +96,13 @@ class ProvidersTableRow extends Component {
|
|||
disabled={isDefaultMapping}
|
||||
/>
|
||||
</div>
|
||||
{isDeleting ? (
|
||||
<ConfirmButtons
|
||||
item={mapping}
|
||||
onCancel={this.handleDismissDeleteConfirmation}
|
||||
onConfirm={this.handleDeleteMap}
|
||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||
confirmTitle="Delete"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-sm btn-default btn-square"
|
||||
onClick={this.handleDeleteClick}
|
||||
>
|
||||
<span className="icon trash" />
|
||||
</button>
|
||||
)}
|
||||
<ConfirmButton
|
||||
square={true}
|
||||
icon="trash"
|
||||
confirmAction={this.handleDeleteMap}
|
||||
confirmText="Delete this Mapping?"
|
||||
disabled={isDefaultMapping}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import ConfirmButtons from 'src/shared/components/ConfirmButtons'
|
||||
import ConfirmOrCancel from 'src/shared/components/ConfirmOrCancel'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import InputClickToEdit from 'src/shared/components/InputClickToEdit'
|
||||
|
||||
|
@ -89,7 +89,7 @@ class ProvidersTableRowNew extends PureComponent<Props, State> {
|
|||
<div className="fancytable--td provider--arrow">
|
||||
<span />
|
||||
</div>
|
||||
<div className="fancytable--td provider--redirect deleting">
|
||||
<div className="fancytable--td provider--redirect creating">
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
onChoose={this.handleChooseOrganization}
|
||||
|
@ -97,7 +97,7 @@ class ProvidersTableRowNew extends PureComponent<Props, State> {
|
|||
className="dropdown-stretch"
|
||||
/>
|
||||
</div>
|
||||
<ConfirmButtons
|
||||
<ConfirmOrCancel
|
||||
onCancel={onCancel}
|
||||
onConfirm={this.handleSaveNewMapping}
|
||||
isDisabled={preventCreate}
|
||||
|
|
|
@ -58,14 +58,16 @@ const UsersTableRow = ({
|
|||
</td>
|
||||
<td style={{width: colProvider}}>{user.provider}</td>
|
||||
<td style={{width: colScheme}}>{user.scheme}</td>
|
||||
<ConfirmButton
|
||||
confirmText={removeWarning}
|
||||
confirmAction={wrappedDelete}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Remove"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
<td className="text-right">
|
||||
<ConfirmButton
|
||||
confirmText={removeWarning}
|
||||
confirmAction={wrappedDelete}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Remove"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ export const USERS_TABLE = {
|
|||
colPassword: 186,
|
||||
colRoles: 190,
|
||||
colPermissions: 190,
|
||||
colDelete: 70,
|
||||
colDelete: 110,
|
||||
}
|
||||
export const ROLES_TABLE = {
|
||||
colName: 280,
|
||||
colUsers: 200,
|
||||
colPermissions: 200,
|
||||
colDelete: 70,
|
||||
colDelete: 110,
|
||||
}
|
||||
export const QUERIES_TABLE = {
|
||||
colDatabase: 160,
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
import {notify} from 'shared/actions/notifications'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
||||
import {generateNewDashboardCell} from 'src/dashboards/constants'
|
||||
import {
|
||||
notifyDashboardDeleted,
|
||||
notifyDashboardDeleteFailed,
|
||||
|
@ -201,10 +201,13 @@ export const getDashboardsAsync = () => async dispatch => {
|
|||
|
||||
const removeUnselectedTemplateValues = dashboard => {
|
||||
const templates = dashboard.templates.map(template => {
|
||||
const values =
|
||||
template.type === 'csv'
|
||||
? template.values
|
||||
: [template.values.find(val => val.selected)] || []
|
||||
if (template.type === 'csv') {
|
||||
return template
|
||||
}
|
||||
|
||||
const value = template.values.find(val => val.selected)
|
||||
const values = value ? [value] : []
|
||||
|
||||
return {...template, values}
|
||||
})
|
||||
return templates
|
||||
|
@ -277,7 +280,7 @@ export const addDashboardCellAsync = dashboard => async dispatch => {
|
|||
try {
|
||||
const {data} = await addDashboardCellAJAX(
|
||||
dashboard,
|
||||
NEW_DEFAULT_DASHBOARD_CELL
|
||||
generateNewDashboardCell(dashboard)
|
||||
)
|
||||
dispatch(addDashboardCell(dashboard, data))
|
||||
} catch (error) {
|
||||
|
|
|
@ -25,6 +25,7 @@ class DashboardsPageContents extends Component {
|
|||
dashboards,
|
||||
onDeleteDashboard,
|
||||
onCreateDashboard,
|
||||
onCloneDashboard,
|
||||
dashboardLink,
|
||||
} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
@ -69,6 +70,7 @@ class DashboardsPageContents extends Component {
|
|||
dashboards={filteredDashboards}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onCreateDashboard={onCreateDashboard}
|
||||
onCloneDashboard={onCloneDashboard}
|
||||
dashboardLink={dashboardLink}
|
||||
/>
|
||||
</div>
|
||||
|
@ -87,6 +89,7 @@ DashboardsPageContents.propTypes = {
|
|||
dashboards: arrayOf(shape()),
|
||||
onDeleteDashboard: func.isRequired,
|
||||
onCreateDashboard: func.isRequired,
|
||||
onCloneDashboard: func.isRequired,
|
||||
dashboardLink: string.isRequired,
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import _ from 'lodash'
|
|||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
|
||||
const AuthorizedEmptyState = ({onCreateDashboard}) => (
|
||||
<div className="generic-empty-state">
|
||||
|
@ -33,6 +33,7 @@ const DashboardsTable = ({
|
|||
dashboards,
|
||||
onDeleteDashboard,
|
||||
onCreateDashboard,
|
||||
onCloneDashboard,
|
||||
dashboardLink,
|
||||
}) => {
|
||||
return dashboards && dashboards.length ? (
|
||||
|
@ -67,11 +68,22 @@ const DashboardsTable = ({
|
|||
requiredRole={EDITOR_ROLE}
|
||||
replaceWithIfNotAuthorized={<td />}
|
||||
>
|
||||
<DeleteConfirmTableCell
|
||||
onDelete={onDeleteDashboard}
|
||||
item={dashboard}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
<td className="text-right">
|
||||
<button
|
||||
className="btn btn-xs btn-default table--show-on-row-hover"
|
||||
onClick={onCloneDashboard(dashboard)}
|
||||
>
|
||||
<span className="icon duplicate" />
|
||||
Clone
|
||||
</button>
|
||||
<ConfirmButton
|
||||
confirmAction={onDeleteDashboard(dashboard)}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Delete"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</Authorized>
|
||||
</tr>
|
||||
))}
|
||||
|
@ -93,6 +105,7 @@ DashboardsTable.propTypes = {
|
|||
dashboards: arrayOf(shape()),
|
||||
onDeleteDashboard: func.isRequired,
|
||||
onCreateDashboard: func.isRequired,
|
||||
onCloneDashboard: func.isRequired,
|
||||
dashboardLink: string.isRequired,
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import ConfirmOrCancel from 'shared/components/ConfirmOrCancel'
|
||||
import SourceSelector from 'src/dashboards/components/SourceSelector'
|
||||
|
||||
const OverlayControls = ({
|
||||
|
@ -44,7 +44,7 @@ const OverlayControls = ({
|
|||
</li>
|
||||
</ul>
|
||||
<div className="overlay-controls--right">
|
||||
<ConfirmButtons
|
||||
<ConfirmOrCancel
|
||||
onCancel={onCancel}
|
||||
onConfirm={onSave}
|
||||
isDisabled={!isSavable}
|
||||
|
|
|
@ -12,7 +12,7 @@ import Dropdown from 'shared/components/Dropdown'
|
|||
import TemplateQueryBuilder from 'src/dashboards/components/template_variables/TemplateQueryBuilder'
|
||||
import TableInput from 'src/dashboards/components/template_variables/TableInput'
|
||||
import RowValues from 'src/dashboards/components/template_variables/RowValues'
|
||||
import RowButtons from 'src/dashboards/components/template_variables/RowButtons'
|
||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
|
||||
import {runTemplateVariableQuery as runTemplateVariableQueryAJAX} from 'src/dashboards/apis'
|
||||
|
||||
|
@ -95,15 +95,31 @@ const TemplateVariableRow = ({
|
|||
autoFocusTarget={autoFocusTarget}
|
||||
/>
|
||||
</div>
|
||||
<div className="tvm--col-4">
|
||||
<RowButtons
|
||||
onStartEdit={onStartEdit}
|
||||
isEditing={isEditing}
|
||||
onCancelEdit={onCancelEdit}
|
||||
onDelete={onDeleteTempVar}
|
||||
id={id}
|
||||
selectedType={selectedType}
|
||||
/>
|
||||
<div className={`tvm--col-4${isEditing ? ' editing' : ''}`}>
|
||||
{isEditing ? (
|
||||
<div className="tvm-actions">
|
||||
<button
|
||||
className="btn btn-sm btn-info btn-square"
|
||||
type="button"
|
||||
onClick={onCancelEdit}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button className="btn btn-sm btn-success btn-square" type="submit">
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tvm-actions">
|
||||
<ConfirmButton
|
||||
type="btn-danger"
|
||||
confirmText="Delete template variable?"
|
||||
confirmAction={onDeleteTempVar(id)}
|
||||
icon="trash"
|
||||
square={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import DeleteConfirmButtons from 'shared/components/DeleteConfirmButtons'
|
||||
|
||||
const RowButtons = ({onStartEdit, isEditing, onCancelEdit, onDelete, id}) => {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="tvm-actions">
|
||||
<button
|
||||
className="btn btn-sm btn-info btn-square"
|
||||
type="button"
|
||||
onClick={onCancelEdit}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button className="btn btn-sm btn-success btn-square" type="submit">
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="tvm-actions">
|
||||
<DeleteConfirmButtons
|
||||
onDelete={onDelete(id)}
|
||||
icon="remove"
|
||||
square={true}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-info btn-edit btn-square"
|
||||
type="button"
|
||||
onClick={onStartEdit('tempVar')}
|
||||
>
|
||||
<span className="icon pencil" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {bool, func, string} = PropTypes
|
||||
|
||||
RowButtons.propTypes = {
|
||||
onStartEdit: func.isRequired,
|
||||
isEditing: bool.isRequired,
|
||||
onCancelEdit: func.isRequired,
|
||||
onDelete: func.isRequired,
|
||||
id: string.isRequired,
|
||||
selectedType: string.isRequired,
|
||||
}
|
||||
|
||||
export default RowButtons
|
|
@ -25,6 +25,49 @@ export const NEW_DEFAULT_DASHBOARD_CELL = {
|
|||
tableOptions: DEFAULT_TABLE_OPTIONS,
|
||||
}
|
||||
|
||||
const getMostCommonValue = values => {
|
||||
const results = values.reduce(
|
||||
(acc, value) => {
|
||||
const {distribution, mostCommonCount} = acc
|
||||
distribution[value] = (distribution[value] || 0) + 1
|
||||
if (distribution[value] > mostCommonCount) {
|
||||
return {
|
||||
distribution,
|
||||
mostCommonCount: distribution[value],
|
||||
mostCommonValue: value,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{distribution: {}, mostCommonCount: 0}
|
||||
)
|
||||
|
||||
return results.mostCommonValue
|
||||
}
|
||||
|
||||
export const generateNewDashboardCell = dashboard => {
|
||||
if (dashboard.cells.length === 0) {
|
||||
return NEW_DEFAULT_DASHBOARD_CELL
|
||||
}
|
||||
|
||||
const newCellY = dashboard.cells
|
||||
.map(cell => cell.y + cell.h)
|
||||
.reduce((a, b) => (a > b ? a : b))
|
||||
|
||||
const existingCellWidths = dashboard.cells.map(cell => cell.w)
|
||||
const existingCellHeights = dashboard.cells.map(cell => cell.h)
|
||||
|
||||
const mostCommonCellWidth = getMostCommonValue(existingCellWidths)
|
||||
const mostCommonCellHeight = getMostCommonValue(existingCellHeights)
|
||||
|
||||
return {
|
||||
...NEW_DEFAULT_DASHBOARD_CELL,
|
||||
y: newCellY,
|
||||
w: mostCommonCellWidth,
|
||||
h: mostCommonCellHeight,
|
||||
}
|
||||
}
|
||||
|
||||
export const NEW_DASHBOARD = {
|
||||
name: 'Name This Dashboard',
|
||||
cells: [NEW_DEFAULT_DASHBOARD_CELL],
|
||||
|
|
|
@ -23,7 +23,16 @@ class DashboardsPage extends Component {
|
|||
push(`/sources/${id}/dashboards/${data.id}`)
|
||||
}
|
||||
|
||||
handleDeleteDashboard = dashboard => {
|
||||
handleCloneDashboard = dashboard => async () => {
|
||||
const {source: {id}, router: {push}} = this.props
|
||||
const {data} = await createDashboard({
|
||||
...dashboard,
|
||||
name: `${dashboard.name} (clone)`,
|
||||
})
|
||||
push(`/sources/${id}/dashboards/${data.id}`)
|
||||
}
|
||||
|
||||
handleDeleteDashboard = dashboard => () => {
|
||||
this.props.handleDeleteDashboard(dashboard)
|
||||
}
|
||||
|
||||
|
@ -39,6 +48,7 @@ class DashboardsPage extends Component {
|
|||
dashboards={dashboards}
|
||||
onDeleteDashboard={this.handleDeleteDashboard}
|
||||
onCreateDashboard={this.handleCreateDashbord}
|
||||
onCloneDashboard={this.handleCloneDashboard}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -278,27 +278,37 @@ export default function ui(state = initialState, action) {
|
|||
case 'EDIT_TEMPLATE_VARIABLE_VALUES': {
|
||||
const {dashboardID, templateID, values} = action.payload
|
||||
|
||||
const dashboards = state.dashboards.map(
|
||||
dashboard =>
|
||||
dashboard.id === dashboardID
|
||||
? {
|
||||
...dashboard,
|
||||
templates: dashboard.templates.map(
|
||||
template =>
|
||||
template.id === templateID && template.type !== 'csv'
|
||||
? {
|
||||
...template,
|
||||
values: values.map(value => ({
|
||||
selected: template.values[0].value === value,
|
||||
value,
|
||||
type: TEMPLATE_VARIABLE_TYPES[template.type],
|
||||
})),
|
||||
}
|
||||
: template
|
||||
),
|
||||
}
|
||||
: dashboard
|
||||
)
|
||||
const dashboards = state.dashboards.map(dashboard => {
|
||||
if (dashboard.id !== dashboardID) {
|
||||
return dashboard
|
||||
}
|
||||
|
||||
const templates = dashboard.templates.map(template => {
|
||||
if (template.id !== templateID && template.type !== 'csv') {
|
||||
return template
|
||||
}
|
||||
|
||||
const selectedValue = _.get(template, 'values', []).find(
|
||||
v => v.selected
|
||||
)
|
||||
|
||||
const v = values.map(value => ({
|
||||
selected: _.get(selectedValue, 'value') === value,
|
||||
value,
|
||||
type: TEMPLATE_VARIABLE_TYPES[template.type],
|
||||
}))
|
||||
|
||||
return {
|
||||
...template,
|
||||
values: v,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...dashboard,
|
||||
templates,
|
||||
}
|
||||
})
|
||||
|
||||
return {...state, dashboards}
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react'
|
||||
import ClusterError from 'shared/components/ClusterError'
|
||||
import PanelHeading from 'shared/components/PanelHeading'
|
||||
import PanelBody from 'shared/components/PanelBody'
|
||||
import errorCopy from 'hson!shared/copy/errors.hson'
|
||||
|
||||
const NoDataNodeError = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<ClusterError>
|
||||
<PanelHeading>{errorCopy.noData.head}</PanelHeading>
|
||||
<PanelBody>{errorCopy.noData.body}</PanelBody>
|
||||
</ClusterError>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default NoDataNodeError
|
|
@ -1,29 +1,21 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const QueryMakerTab = React.createClass({
|
||||
propTypes: {
|
||||
isActive: PropTypes.bool,
|
||||
query: PropTypes.shape({
|
||||
rawText: PropTypes.string,
|
||||
}).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
queryTabText: PropTypes.string,
|
||||
queryIndex: PropTypes.number,
|
||||
},
|
||||
interface Query {
|
||||
rawText: string
|
||||
}
|
||||
|
||||
handleSelect() {
|
||||
this.props.onSelect(this.props.queryIndex)
|
||||
},
|
||||
interface Props {
|
||||
isActive: boolean
|
||||
query: Query
|
||||
onSelect: (index: number) => void
|
||||
onDelete: (index: number) => void
|
||||
queryTabText: string
|
||||
queryIndex: number
|
||||
}
|
||||
|
||||
handleDelete(e) {
|
||||
e.stopPropagation()
|
||||
this.props.onDelete(this.props.queryIndex)
|
||||
},
|
||||
|
||||
render() {
|
||||
class QueryMakerTab extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div
|
||||
className={classnames('query-maker--tab', {
|
||||
|
@ -39,7 +31,16 @@ const QueryMakerTab = React.createClass({
|
|||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private handleSelect() {
|
||||
this.props.onSelect(this.props.queryIndex)
|
||||
}
|
||||
|
||||
private handleDelete(e) {
|
||||
e.stopPropagation()
|
||||
this.props.onDelete(this.props.queryIndex)
|
||||
}
|
||||
}
|
||||
|
||||
export default QueryMakerTab
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {withRouter} from 'react-router'
|
||||
import DataExplorer from './DataExplorer'
|
||||
|
||||
const App = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string.isRequired,
|
||||
self: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="page">
|
||||
<DataExplorer source={this.props.source} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default withRouter(App)
|
|
@ -0,0 +1,21 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {withRouter} from 'react-router'
|
||||
import DataExplorer from './DataExplorer'
|
||||
|
||||
import {Source} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
}
|
||||
|
||||
class DataExplorerPage extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="page">
|
||||
<DataExplorer source={this.props.source} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(DataExplorerPage)
|
|
@ -1,2 +1,2 @@
|
|||
import App from './containers/App'
|
||||
export default App
|
||||
import DataExplorerPage from './containers/DataExplorerPage'
|
||||
export default DataExplorerPage
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
export const getSuggestions = async (url: string) => {
|
||||
try {
|
||||
const {data} = await AJAX({
|
||||
url,
|
||||
})
|
||||
|
||||
return data.funcs
|
||||
} catch (error) {
|
||||
console.error('Could not get suggestions', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
interface ASTRequest {
|
||||
url: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export const getAST = async (request: ASTRequest) => {
|
||||
const {url, body} = request
|
||||
try {
|
||||
const {data} = await AJAX({
|
||||
method: 'POST',
|
||||
url,
|
||||
data: {body},
|
||||
})
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Could not parse query', error)
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// Texas Ranger
|
||||
import _ from 'lodash'
|
||||
|
||||
interface Expression {
|
||||
expression: object
|
||||
}
|
||||
|
||||
interface AST {
|
||||
body: Expression[]
|
||||
}
|
||||
|
||||
export default class Walker {
|
||||
private ast: AST
|
||||
|
||||
constructor(ast) {
|
||||
this.ast = ast
|
||||
}
|
||||
|
||||
public get functions() {
|
||||
return this.buildFuncNodes(this.walk(this.baseExpression))
|
||||
}
|
||||
|
||||
private reduceArgs = args => {
|
||||
if (!args) {
|
||||
return []
|
||||
}
|
||||
|
||||
return args.reduce(
|
||||
(acc, arg) => [...acc, ...this.getProperties(arg.properties)],
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
private walk = currentNode => {
|
||||
if (_.isEmpty(currentNode)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let name
|
||||
let args
|
||||
if (currentNode.call) {
|
||||
name = currentNode.call.callee.name
|
||||
args = currentNode.call.arguments
|
||||
return [...this.walk(currentNode.argument), {name, args}]
|
||||
}
|
||||
|
||||
name = currentNode.callee.name
|
||||
args = currentNode.arguments
|
||||
return [{name, args}]
|
||||
}
|
||||
|
||||
private buildFuncNodes = nodes => {
|
||||
return nodes.map(node => {
|
||||
return {
|
||||
name: node.name,
|
||||
arguments: this.reduceArgs(node.args),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private getProperties = props => {
|
||||
return props.map(prop => ({
|
||||
key: prop.key.name,
|
||||
value: _.get(
|
||||
prop,
|
||||
'value.value',
|
||||
_.get(prop, 'value.location.source', '')
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
private get baseExpression() {
|
||||
return _.get(this.ast, 'body.0.expression', {})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
|
||||
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import DropdownInput from 'src/shared/components/DropdownInput'
|
||||
import FuncListItem from 'src/ifql/components/FuncListItem'
|
||||
|
||||
interface Props {
|
||||
inputText: string
|
||||
isOpen: boolean
|
||||
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void
|
||||
onAddNode: (name: string) => void
|
||||
funcs: string[]
|
||||
}
|
||||
|
||||
const FuncList: SFC<Props> = ({
|
||||
inputText,
|
||||
isOpen,
|
||||
onAddNode,
|
||||
onKeyDown,
|
||||
onInputChange,
|
||||
funcs,
|
||||
}) => {
|
||||
return (
|
||||
<ul className="dropdown-menu funcs">
|
||||
<DropdownInput
|
||||
buttonSize="btn-xs"
|
||||
buttonColor="btn-default"
|
||||
onFilterChange={onInputChange}
|
||||
onFilterKeyPress={onKeyDown}
|
||||
searchTerm={inputText}
|
||||
/>
|
||||
<FancyScrollbar autoHide={false} autoHeight={true} maxHeight={240}>
|
||||
{isOpen &&
|
||||
funcs.map((func, i) => (
|
||||
<FuncListItem key={i} name={func} onAddNode={onAddNode} />
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default FuncList
|
|
@ -0,0 +1,22 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
onAddNode: (name: string) => void
|
||||
}
|
||||
|
||||
export default class FuncListItem extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {name} = this.props
|
||||
|
||||
return (
|
||||
<li onClick={this.handleClick} className="dropdown-item func">
|
||||
<a>{name}</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.props.onAddNode(this.props.name)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import FuncList from 'src/ifql/components/FuncList'
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
inputText: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
funcs: string[]
|
||||
onAddNode: (name: string) => void
|
||||
}
|
||||
|
||||
export class FuncSelector extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
inputText: '',
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {isOpen, inputText} = this.state
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={`dropdown dashboard-switcher ${this.openClass}`}>
|
||||
<button
|
||||
className="btn btn-square btn-default btn-sm dropdown-toggle"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span className="icon plus" />
|
||||
</button>
|
||||
<FuncList
|
||||
inputText={inputText}
|
||||
onAddNode={this.handleAddNode}
|
||||
isOpen={isOpen}
|
||||
funcs={this.availableFuncs}
|
||||
onInputChange={this.handleInputChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
private get openClass(): string {
|
||||
if (this.state.isOpen) {
|
||||
return 'open'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private handleAddNode = (name: string) => {
|
||||
this.setState({isOpen: false})
|
||||
this.props.onAddNode(name)
|
||||
}
|
||||
|
||||
private get availableFuncs(): string[] {
|
||||
return this.props.funcs.filter(f =>
|
||||
f.toLowerCase().includes(this.state.inputText)
|
||||
)
|
||||
}
|
||||
|
||||
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({inputText: e.target.value})
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({inputText: '', isOpen: false})
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private handleClickOutside = () => {
|
||||
this.setState({isOpen: false})
|
||||
}
|
||||
}
|
||||
|
||||
export default FuncSelector
|
|
@ -0,0 +1,25 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
interface Arg {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Node {
|
||||
name: string
|
||||
arguments: Arg[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
node: Node
|
||||
}
|
||||
|
||||
const Node: SFC<Props> = ({node}) => {
|
||||
return (
|
||||
<div className="func-node">
|
||||
<div>{node.name}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Node
|
|
@ -0,0 +1,32 @@
|
|||
import React, {SFC} from 'react'
|
||||
import FuncSelector from 'src/ifql/components/FuncSelector'
|
||||
import Node from 'src/ifql/components/Node'
|
||||
|
||||
interface Arg {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface NodeProp {
|
||||
name: string
|
||||
arguments: Arg[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
funcs: string[]
|
||||
nodes: NodeProp[]
|
||||
onAddNode: (name: string) => void
|
||||
}
|
||||
|
||||
const TimeMachine: SFC<Props> = ({funcs, nodes, onAddNode}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="func-node-container">
|
||||
{nodes.map((n, i) => <Node key={i} node={n} />)}
|
||||
<FuncSelector funcs={funcs} onAddNode={onAddNode} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimeMachine
|
|
@ -0,0 +1,155 @@
|
|||
export const ast = {
|
||||
type: 'File',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
program: {
|
||||
type: 'Program',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
expression: {
|
||||
type: 'CallExpression',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 4,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 4,
|
||||
},
|
||||
identifierName: 'from',
|
||||
},
|
||||
name: 'from',
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'ObjectExpression',
|
||||
start: 5,
|
||||
end: 21,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 5,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 21,
|
||||
},
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
type: 'ObjectProperty',
|
||||
start: 6,
|
||||
end: 20,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 6,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 20,
|
||||
},
|
||||
},
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
start: 6,
|
||||
end: 8,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 6,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 8,
|
||||
},
|
||||
identifierName: 'db',
|
||||
},
|
||||
name: 'db',
|
||||
},
|
||||
value: {
|
||||
type: 'StringLiteral',
|
||||
start: 10,
|
||||
end: 20,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 10,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 20,
|
||||
},
|
||||
},
|
||||
extra: {
|
||||
rawValue: 'telegraf',
|
||||
raw: 'telegraf',
|
||||
},
|
||||
value: 'telegraf',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
directives: [],
|
||||
},
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import TimeMachine from 'src/ifql/components/TimeMachine'
|
||||
import Walker from 'src/ifql/ast/walker'
|
||||
|
||||
import {getSuggestions, getAST} from 'src/ifql/apis'
|
||||
|
||||
interface Links {
|
||||
self: string
|
||||
suggestions: string
|
||||
ast: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
links: Links
|
||||
}
|
||||
|
||||
interface State {
|
||||
funcs: string[]
|
||||
ast: object
|
||||
query: string
|
||||
}
|
||||
|
||||
export class IFQLPage extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
funcs: [],
|
||||
ast: null,
|
||||
query: 'from(db: "telegraf") |> filter() |> range()',
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const {links} = this.props
|
||||
const {suggestions} = links
|
||||
|
||||
try {
|
||||
const results = await getSuggestions(suggestions)
|
||||
const funcs = results.map(s => s.name)
|
||||
this.setState({funcs})
|
||||
} catch (error) {
|
||||
console.error('Could not get function suggestions: ', error)
|
||||
}
|
||||
|
||||
this.getASTResponse(this.state.query)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {funcs} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Time Machine</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<TimeMachine
|
||||
funcs={funcs}
|
||||
nodes={this.nodes}
|
||||
onAddNode={this.handleAddNode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleAddNode = (name: string) => {
|
||||
const query = `${this.state.query} |> ${name}()`
|
||||
this.getASTResponse(query)
|
||||
}
|
||||
|
||||
private get nodes() {
|
||||
const {ast} = this.state
|
||||
|
||||
if (!ast) {
|
||||
return []
|
||||
}
|
||||
|
||||
const walker = new Walker(ast)
|
||||
|
||||
return walker.functions
|
||||
}
|
||||
|
||||
private async getASTResponse(query: string) {
|
||||
const {links} = this.props
|
||||
|
||||
try {
|
||||
const ast = await getAST({url: links.ast, body: query})
|
||||
this.setState({ast, query})
|
||||
} catch (error) {
|
||||
console.error('Could not parse AST', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({links}) => {
|
||||
return {links: links.ifql}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(IFQLPage)
|
|
@ -0,0 +1,3 @@
|
|||
import IFQLPage from 'src/ifql/containers/IFQLPage'
|
||||
|
||||
export {IFQLPage}
|
|
@ -1,6 +1,6 @@
|
|||
import 'babel-polyfill'
|
||||
|
||||
import React from 'react'
|
||||
import React, {PureComponent} from 'react'
|
||||
import {render} from 'react-dom'
|
||||
import {Provider} from 'react-redux'
|
||||
import {Router, Route, useRouterHistory} from 'react-router'
|
||||
|
@ -32,26 +32,34 @@ import {
|
|||
} from 'src/kapacitor'
|
||||
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
|
||||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
import NotFound from 'shared/components/NotFound'
|
||||
import {IFQLPage} from 'src/ifql/index'
|
||||
import NotFound from 'src/shared/components/NotFound'
|
||||
|
||||
import {getLinksAsync} from 'shared/actions/links'
|
||||
import {getMeAsync} from 'shared/actions/auth'
|
||||
import {getLinksAsync} from 'src/shared/actions/links'
|
||||
import {getMeAsync} from 'src/shared/actions/auth'
|
||||
|
||||
import {disablePresentationMode} from 'shared/actions/app'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
import {notify} from 'shared/actions/notifications'
|
||||
import {disablePresentationMode} from 'src/shared/actions/app'
|
||||
import {errorThrown} from 'src/shared/actions/errors'
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
|
||||
import 'src/style/chronograf.scss'
|
||||
|
||||
import {HEARTBEAT_INTERVAL} from 'shared/constants'
|
||||
import {HEARTBEAT_INTERVAL} from 'src/shared/constants'
|
||||
|
||||
const errorsQueue = []
|
||||
|
||||
const rootNode = document.getElementById('react-root')
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
basepath: string
|
||||
}
|
||||
}
|
||||
|
||||
// Older method used for pre-IE 11 compatibility
|
||||
const basepath = rootNode.getAttribute('data-basepath') || ''
|
||||
window.basepath = basepath
|
||||
|
||||
const browserHistory = useRouterHistory(createHistory)({
|
||||
basename: basepath, // this is written in when available by the URL prefixer middleware
|
||||
})
|
||||
|
@ -73,14 +81,22 @@ window.addEventListener('keyup', event => {
|
|||
|
||||
const history = syncHistoryWithStore(browserHistory, store)
|
||||
|
||||
const Root = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
interface State {
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
class Root extends PureComponent<{}, State> {
|
||||
private getLinks = bindActionCreators(getLinksAsync, dispatch)
|
||||
private getMe = bindActionCreators(getMeAsync, dispatch)
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
ready: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
public async componentWillMount() {
|
||||
this.flushErrorsQueue()
|
||||
|
||||
try {
|
||||
|
@ -90,45 +106,10 @@ const Root = React.createClass({
|
|||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
getLinks: bindActionCreators(getLinksAsync, dispatch),
|
||||
getMe: bindActionCreators(getMeAsync, dispatch),
|
||||
|
||||
async checkAuth() {
|
||||
try {
|
||||
await this.performHeartbeat({shouldResetMe: true})
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
},
|
||||
|
||||
async performHeartbeat({shouldResetMe = false} = {}) {
|
||||
await this.getMe({shouldResetMe})
|
||||
|
||||
setTimeout(() => {
|
||||
if (store.getState().auth.me !== null) {
|
||||
this.performHeartbeat()
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
},
|
||||
|
||||
flushErrorsQueue() {
|
||||
if (errorsQueue.length) {
|
||||
errorsQueue.forEach(error => {
|
||||
if (typeof error === 'object') {
|
||||
dispatch(notify(error))
|
||||
} else {
|
||||
dispatch(errorThrown({status: 0, auth: null}, error, 'warning'))
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
return !this.state.ready ? ( // eslint-disable-line no-negated-condition
|
||||
<div className="page-spinner" />
|
||||
) : (
|
||||
public render() {
|
||||
return this.state.ready ? (
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<Route path="/" component={UserIsAuthenticated(CheckSources)} />
|
||||
|
@ -163,14 +144,47 @@ const Root = React.createClass({
|
|||
<Route path="manage-sources" component={ManageSources} />
|
||||
<Route path="manage-sources/new" component={SourcePage} />
|
||||
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
||||
<Route path="ifql" component={IFQLPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" component={NotFound} />
|
||||
</Router>
|
||||
</Provider>
|
||||
) : (
|
||||
<div className="page-spinner" />
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async performHeartbeat({shouldResetMe = false} = {}) {
|
||||
await this.getMe({shouldResetMe})
|
||||
|
||||
setTimeout(() => {
|
||||
if (store.getState().auth.me !== null) {
|
||||
this.performHeartbeat()
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
}
|
||||
|
||||
private flushErrorsQueue() {
|
||||
if (errorsQueue.length) {
|
||||
errorsQueue.forEach(error => {
|
||||
if (typeof error === 'object') {
|
||||
dispatch(notify(error))
|
||||
} else {
|
||||
dispatch(errorThrown({status: 0, auth: null}, error, 'warning'))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAuth() {
|
||||
try {
|
||||
await this.performHeartbeat({shouldResetMe: true})
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rootNode) {
|
||||
render(<Root />, rootNode)
|
|
@ -1,4 +1,4 @@
|
|||
import AJAX from 'utils/ajax'
|
||||
import AJAX from 'src/utils/ajax'
|
||||
import _ from 'lodash'
|
||||
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import React, {PureComponent, ReactElement} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
interface Props {
|
||||
children: ReactElement<any>
|
||||
onClickOutside: () => void
|
||||
}
|
||||
|
||||
export class ClickOutside extends PureComponent<Props> {
|
||||
public componentDidMount() {
|
||||
document.addEventListener('click', this.handleClickOutside, true)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleClickOutside, true)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return React.Children.only(this.props.children)
|
||||
}
|
||||
|
||||
private handleClickOutside = e => {
|
||||
const domNode = ReactDOM.findDOMNode(this)
|
||||
if (!domNode || !domNode.contains(e.target)) {
|
||||
this.props.onClickOutside()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,16 +18,19 @@ interface CancelProps {
|
|||
buttonSize: string
|
||||
onCancel: () => void
|
||||
icon: string
|
||||
title: string
|
||||
}
|
||||
interface ConfirmButtonsProps {
|
||||
|
||||
interface ConfirmOrCancelProps {
|
||||
onConfirm: (item: Item) => void
|
||||
item: Item
|
||||
onCancel: (item: Item) => void
|
||||
buttonSize?: string
|
||||
isDisabled?: boolean
|
||||
onClickOutside?: (item: Item) => void
|
||||
confirmLeft?: boolean
|
||||
reversed?: boolean
|
||||
confirmTitle?: string
|
||||
cancelTitle?: string
|
||||
}
|
||||
|
||||
export const Confirm: SFC<ConfirmProps> = ({
|
||||
|
@ -39,9 +42,12 @@ export const Confirm: SFC<ConfirmProps> = ({
|
|||
}) => (
|
||||
<button
|
||||
data-test="confirm"
|
||||
className={classnames('btn btn-success btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
className={classnames(
|
||||
'confirm-or-cancel--confirm btn btn-success btn-square',
|
||||
{
|
||||
[buttonSize]: buttonSize,
|
||||
}
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
title={isDisabled ? `Cannot ${title}` : title}
|
||||
onClick={onConfirm}
|
||||
|
@ -50,22 +56,29 @@ export const Confirm: SFC<ConfirmProps> = ({
|
|||
</button>
|
||||
)
|
||||
|
||||
export const Cancel: SFC<CancelProps> = ({buttonSize, onCancel, icon}) => (
|
||||
export const Cancel: SFC<CancelProps> = ({
|
||||
buttonSize,
|
||||
onCancel,
|
||||
icon,
|
||||
title,
|
||||
}) => (
|
||||
<button
|
||||
data-test="cancel"
|
||||
className={classnames('btn btn-info btn-square', {
|
||||
className={classnames('confirm-or-cancel--cancel btn btn-info btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={onCancel}
|
||||
title={title}
|
||||
>
|
||||
<span className={icon} />
|
||||
</button>
|
||||
)
|
||||
|
||||
class ConfirmButtons extends PureComponent<ConfirmButtonsProps, {}> {
|
||||
public static defaultProps: Partial<ConfirmButtonsProps> = {
|
||||
class ConfirmOrCancel extends PureComponent<ConfirmOrCancelProps, {}> {
|
||||
public static defaultProps: Partial<ConfirmOrCancelProps> = {
|
||||
buttonSize: 'btn-sm',
|
||||
confirmTitle: 'Save',
|
||||
cancelTitle: 'Cancel',
|
||||
onClickOutside: () => {},
|
||||
}
|
||||
|
||||
|
@ -86,29 +99,23 @@ class ConfirmButtons extends PureComponent<ConfirmButtonsProps, {}> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {item, buttonSize, isDisabled, confirmLeft, confirmTitle} = this.props
|
||||
const {
|
||||
item,
|
||||
buttonSize,
|
||||
isDisabled,
|
||||
reversed,
|
||||
confirmTitle,
|
||||
cancelTitle,
|
||||
} = this.props
|
||||
|
||||
return confirmLeft ? (
|
||||
<div className="confirm-buttons">
|
||||
<Confirm
|
||||
buttonSize={buttonSize}
|
||||
isDisabled={isDisabled}
|
||||
onConfirm={this.handleConfirm(item)}
|
||||
icon="icon checkmark"
|
||||
title={confirmTitle}
|
||||
/>
|
||||
<Cancel
|
||||
buttonSize={buttonSize}
|
||||
onCancel={this.handleCancel(item)}
|
||||
icon="icon remove"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="confirm-buttons">
|
||||
const className = `confirm-or-cancel${reversed ? ' reversed' : ''}`
|
||||
return (
|
||||
<div className={className}>
|
||||
<Cancel
|
||||
buttonSize={buttonSize}
|
||||
onCancel={this.handleCancel(item)}
|
||||
icon="icon remove"
|
||||
title={cancelTitle}
|
||||
/>
|
||||
<Confirm
|
||||
buttonSize={buttonSize}
|
||||
|
@ -122,4 +129,4 @@ class ConfirmButtons extends PureComponent<ConfirmButtonsProps, {}> {
|
|||
}
|
||||
}
|
||||
|
||||
export default OnClickOutside(ConfirmButtons)
|
||||
export default OnClickOutside(ConfirmOrCancel)
|
|
@ -1,116 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
|
||||
const DeleteButton = ({
|
||||
onClickDelete,
|
||||
buttonSize,
|
||||
icon,
|
||||
square,
|
||||
text,
|
||||
disabled,
|
||||
}) => (
|
||||
<button
|
||||
className={classnames('btn btn-danger table--show-on-row-hover', {
|
||||
[buttonSize]: buttonSize,
|
||||
'btn-square': square,
|
||||
disabled,
|
||||
})}
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
{icon ? <span className={`icon ${icon}`} /> : null}
|
||||
{square ? null : text}
|
||||
</button>
|
||||
)
|
||||
|
||||
class DeleteConfirmButtons extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isConfirming: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleClickDelete = () => {
|
||||
this.setState({isConfirming: true})
|
||||
}
|
||||
|
||||
handleCancel = () => {
|
||||
this.setState({isConfirming: false})
|
||||
}
|
||||
|
||||
handleClickOutside() {
|
||||
this.setState({isConfirming: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onDelete,
|
||||
item,
|
||||
buttonSize,
|
||||
icon,
|
||||
square,
|
||||
text,
|
||||
disabled,
|
||||
} = this.props
|
||||
const {isConfirming} = this.state
|
||||
|
||||
if (square && !icon) {
|
||||
console.error(
|
||||
'DeleteButton component requires both icon if passing in square.'
|
||||
)
|
||||
}
|
||||
|
||||
return isConfirming ? (
|
||||
<ConfirmButtons
|
||||
onConfirm={onDelete}
|
||||
item={item}
|
||||
onCancel={this.handleCancel}
|
||||
buttonSize={buttonSize}
|
||||
/>
|
||||
) : (
|
||||
<DeleteButton
|
||||
text={text}
|
||||
onClickDelete={disabled ? () => {} : this.handleClickDelete}
|
||||
buttonSize={buttonSize}
|
||||
icon={icon}
|
||||
square={square}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {bool, func, oneOfType, shape, string} = PropTypes
|
||||
|
||||
DeleteButton.propTypes = {
|
||||
onClickDelete: func.isRequired,
|
||||
buttonSize: string,
|
||||
icon: string,
|
||||
square: bool,
|
||||
disabled: bool,
|
||||
text: string.isRequired,
|
||||
}
|
||||
|
||||
DeleteButton.defaultProps = {
|
||||
text: 'Delete',
|
||||
}
|
||||
|
||||
DeleteConfirmButtons.propTypes = {
|
||||
text: string,
|
||||
item: oneOfType([(string, shape())]),
|
||||
onDelete: func.isRequired,
|
||||
buttonSize: string,
|
||||
square: bool,
|
||||
icon: string,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
DeleteConfirmButtons.defaultProps = {
|
||||
buttonSize: 'btn-sm',
|
||||
}
|
||||
|
||||
export default OnClickOutside(DeleteConfirmButtons)
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import DeleteConfirmButtons from 'shared/components/DeleteConfirmButtons'
|
||||
import {ADMIN_TABLE} from 'src/admin/constants/tableSizing'
|
||||
|
||||
const DeleteConfirmTableCell = props => (
|
||||
<td className="text-right" style={{width: `${ADMIN_TABLE.colDelete}px`}}>
|
||||
<DeleteConfirmButtons {...props} />
|
||||
</td>
|
||||
)
|
||||
|
||||
export default DeleteConfirmTableCell
|
|
@ -1,9 +1,21 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
|
||||
|
||||
const disabledClass = disabled => (disabled ? ' disabled' : '')
|
||||
|
||||
const DropdownInput = ({
|
||||
type OnFilterChangeHandler = (e: ChangeEvent<HTMLInputElement>) => void
|
||||
type OnFilterKeyPress = (e: KeyboardEvent<HTMLInputElement>) => void
|
||||
|
||||
interface Props {
|
||||
searchTerm: string
|
||||
buttonSize: string
|
||||
buttonColor: string
|
||||
toggleStyle?: object
|
||||
disabled?: boolean
|
||||
onFilterChange: OnFilterChangeHandler
|
||||
onFilterKeyPress: OnFilterKeyPress
|
||||
}
|
||||
|
||||
const DropdownInput: SFC<Props> = ({
|
||||
searchTerm,
|
||||
buttonSize,
|
||||
buttonColor,
|
||||
|
@ -33,15 +45,3 @@ const DropdownInput = ({
|
|||
)
|
||||
|
||||
export default DropdownInput
|
||||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
DropdownInput.propTypes = {
|
||||
searchTerm: string,
|
||||
buttonSize: string,
|
||||
buttonColor: string,
|
||||
toggleStyle: shape({}),
|
||||
disabled: bool,
|
||||
onFilterChange: func.isRequired,
|
||||
onFilterKeyPress: func.isRequired,
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import {SFC} from 'react'
|
||||
|
||||
interface Props {
|
||||
name?: string
|
||||
children?: any
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
const GraphTips = React.createClass({
|
||||
render() {
|
||||
const graphTipsText =
|
||||
'<h1>Graph Tips:</h1><p><code>Click + Drag</code> Zoom in (X or Y)<br/><code>Shift + Click</code> Pan Graph Window<br/><code>Double Click</code> Reset Graph Window</p><h1>Static Legend Tips:</h1><p><code>Click</code>Focus on single Series<br/><code>Shift + Click</code> Show/Hide single Series</p>'
|
||||
return (
|
||||
<div
|
||||
className="graph-tips"
|
||||
data-for="graph-tips-tooltip"
|
||||
data-tip={graphTipsText}
|
||||
>
|
||||
<span>?</span>
|
||||
<ReactTooltip
|
||||
id="graph-tips-tooltip"
|
||||
effect="solid"
|
||||
html={true}
|
||||
place="bottom"
|
||||
class="influx-tooltip"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default GraphTips
|
|
@ -0,0 +1,24 @@
|
|||
import React, {SFC} from 'react'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
const graphTipsText =
|
||||
'<h1>Graph Tips:</h1><p><code>Click + Drag</code> Zoom in (X or Y)<br/><code>Shift + Click</code> Pan Graph Window<br/><code>Double Click</code> Reset Graph Window</p><h1>Static Legend Tips:</h1><p><code>Click</code>Focus on single Series<br/><code>Shift + Click</code> Show/Hide single Series</p>'
|
||||
|
||||
const GraphTips: SFC = () => (
|
||||
<div
|
||||
className="graph-tips"
|
||||
data-for="graph-tips-tooltip"
|
||||
data-tip={graphTipsText}
|
||||
>
|
||||
<span>?</span>
|
||||
<ReactTooltip
|
||||
id="graph-tips-tooltip"
|
||||
effect="solid"
|
||||
html={true}
|
||||
place="bottom"
|
||||
class="influx-tooltip"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default GraphTips
|
|
@ -4,7 +4,7 @@ import Dygraph from 'shared/components/Dygraph'
|
|||
import shallowCompare from 'react-addons-shallow-compare'
|
||||
|
||||
import SingleStat from 'src/shared/components/SingleStat'
|
||||
import timeSeriesToDygraph from 'utils/timeSeriesToDygraph'
|
||||
import timeSeriesToDygraph from 'utils/timeSeriesTransformers'
|
||||
|
||||
import {SINGLE_STAT_LINE_COLORS} from 'src/shared/graphs/helpers'
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
|
|||
import Dygraph from './Dygraph'
|
||||
import shallowCompare from 'react-addons-shallow-compare'
|
||||
|
||||
import timeSeriesToDygraph from 'utils/timeSeriesToDygraph'
|
||||
import timeSeriesToDygraph from 'utils/timeSeriesTransformers'
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'MiniGraph',
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
const NoKapacitorError = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
const path = `/sources/${this.props.source.id}/kapacitors/new`
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
<p>
|
||||
The current source does not have an associated Kapacitor instance
|
||||
<br />
|
||||
<br />
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<Link to={path} className="btn btn-sm btn-primary">
|
||||
Configure Kapacitor
|
||||
</Link>
|
||||
</Authorized>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default NoKapacitorError
|
|
@ -0,0 +1,30 @@
|
|||
import React, {SFC} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
interface Props {
|
||||
source: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
const NoKapacitorError: SFC<Props> = ({source}) => {
|
||||
const path = `/sources/${source.id}/kapacitors/new`
|
||||
return (
|
||||
<div className="graph-empty">
|
||||
<p>
|
||||
The current source does not have an associated Kapacitor instance
|
||||
<br />
|
||||
<br />
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<Link to={path} className="btn btn-sm btn-primary">
|
||||
Configure Kapacitor
|
||||
</Link>
|
||||
</Authorized>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoKapacitorError
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
const NotFound = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="panel">
|
||||
<div className="panel-heading text-center">
|
||||
<h1 className="deluxe">404</h1>
|
||||
<h4>Bummer! We couldn't find the page you were looking for</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default NotFound
|
|
@ -0,0 +1,13 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
const NotFound: SFC = () => (
|
||||
<div className="container-fluid">
|
||||
<div className="panel">
|
||||
<div className="panel-heading text-center">
|
||||
<h1 className="deluxe">404</h1>
|
||||
<h4>Bummer! We couldn't find the page you were looking for</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
export default NotFound
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const {node} = PropTypes
|
||||
const PanelBody = React.createClass({
|
||||
propTypes: {
|
||||
children: node.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="panel-body text-center">
|
||||
<h3 className="deluxe">How to resolve:</h3>
|
||||
<p>{this.props.children}</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default PanelBody
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const {node} = PropTypes
|
||||
const PanelHeading = React.createClass({
|
||||
propTypes: {
|
||||
children: node.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="panel-heading text-center">
|
||||
<h2 className="deluxe">{this.props.children}</h2>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default PanelHeading
|
|
@ -1,30 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const {func, bool, string} = PropTypes
|
||||
const ResizeHandle = React.createClass({
|
||||
propTypes: {
|
||||
onHandleStartDrag: func.isRequired,
|
||||
isDragging: bool.isRequired,
|
||||
theme: string,
|
||||
top: string,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {isDragging, onHandleStartDrag, top, theme} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('resizer--handle', {
|
||||
dragging: isDragging,
|
||||
'resizer--malachite': theme === 'kapacitor',
|
||||
})}
|
||||
onMouseDown={onHandleStartDrag}
|
||||
style={{top}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default ResizeHandle
|
|
@ -0,0 +1,27 @@
|
|||
import React, {SFC} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface Props {
|
||||
onHandleStartDrag: () => void
|
||||
isDragging: boolean
|
||||
theme?: string
|
||||
top?: string
|
||||
}
|
||||
|
||||
const ResizeHandle: SFC<Props> = ({
|
||||
onHandleStartDrag,
|
||||
isDragging,
|
||||
theme,
|
||||
top,
|
||||
}) => (
|
||||
<div
|
||||
className={classnames('resizer--handle', {
|
||||
dragging: isDragging,
|
||||
'resizer--malachite': theme === 'kapacitor',
|
||||
})}
|
||||
onMouseDown={onHandleStartDrag}
|
||||
style={{top}}
|
||||
/>
|
||||
)
|
||||
|
||||
export default ResizeHandle
|
|
@ -5,8 +5,13 @@ import classnames from 'classnames'
|
|||
|
||||
import {MultiGrid, ColumnSizer} from 'react-virtualized'
|
||||
import moment from 'moment'
|
||||
import {reduce} from 'fast.js'
|
||||
|
||||
import {
|
||||
timeSeriesToTableGraph,
|
||||
processTableData,
|
||||
} from 'src/utils/timeSeriesTransformers'
|
||||
|
||||
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesToDygraph'
|
||||
import {
|
||||
NULL_ARRAY_INDEX,
|
||||
NULL_HOVER_TIME,
|
||||
|
@ -14,51 +19,24 @@ import {
|
|||
TIME_FIELD_DEFAULT,
|
||||
ASCENDING,
|
||||
DESCENDING,
|
||||
DEFAULT_SORT,
|
||||
FIX_FIRST_COLUMN_DEFAULT,
|
||||
VERTICAL_TIME_AXIS_DEFAULT,
|
||||
calculateTimeColumnWidth,
|
||||
calculateLabelsColumnWidth,
|
||||
} from 'src/shared/constants/tableGraph'
|
||||
export const DEFAULT_SORT = ASCENDING
|
||||
|
||||
import {generateThresholdsListHexs} from 'shared/constants/colorOperations'
|
||||
|
||||
export const filterInvisibleColumns = (data, fieldNames) => {
|
||||
const visibility = {}
|
||||
const filteredData = data.map((row, i) => {
|
||||
return row.filter((col, j) => {
|
||||
if (i === 0) {
|
||||
const foundField = fieldNames.find(field => field.internalName === col)
|
||||
visibility[j] = foundField ? foundField.visible : true
|
||||
}
|
||||
return visibility[j]
|
||||
})
|
||||
})
|
||||
return filteredData[0].length ? filteredData : [[]]
|
||||
}
|
||||
|
||||
export const processData = (
|
||||
data,
|
||||
sortFieldName,
|
||||
direction,
|
||||
verticalTimeAxis,
|
||||
fieldNames
|
||||
) => {
|
||||
const sortIndex = _.indexOf(data[0], sortFieldName)
|
||||
const sortedData = [
|
||||
data[0],
|
||||
..._.orderBy(_.drop(data, 1), sortIndex, [direction]),
|
||||
]
|
||||
const sortedTimeVals = sortedData.map(r => r[0])
|
||||
const filteredData = filterInvisibleColumns(sortedData, fieldNames)
|
||||
const processedData = verticalTimeAxis ? filteredData : _.unzip(filteredData)
|
||||
|
||||
return {processedData, sortedTimeVals}
|
||||
}
|
||||
|
||||
class TableGraph extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const sortField = _.get(
|
||||
this.props,
|
||||
['tableOptions', 'sortBy', 'internalName'],
|
||||
TIME_FIELD_DEFAULT.internalName
|
||||
)
|
||||
this.state = {
|
||||
data: [[]],
|
||||
processedData: [[]],
|
||||
|
@ -68,7 +46,7 @@ class TableGraph extends Component {
|
|||
labelsColumnWidth: calculateLabelsColumnWidth(props.data.labels),
|
||||
hoveredColumnIndex: NULL_ARRAY_INDEX,
|
||||
hoveredRowIndex: NULL_ARRAY_INDEX,
|
||||
sortField: 'time',
|
||||
sortField,
|
||||
sortDirection: DEFAULT_SORT,
|
||||
}
|
||||
}
|
||||
|
@ -103,18 +81,17 @@ class TableGraph extends Component {
|
|||
|
||||
let direction, sortFieldName
|
||||
if (
|
||||
_.isEmpty(sortField) ||
|
||||
_.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') !==
|
||||
_.get(nextProps, ['tableOptions', 'sortBy', 'internalName'], '')
|
||||
_.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') ===
|
||||
internalName
|
||||
) {
|
||||
direction = DEFAULT_SORT
|
||||
sortFieldName = internalName
|
||||
} else {
|
||||
direction = sortDirection
|
||||
sortFieldName = sortField
|
||||
} else {
|
||||
direction = DEFAULT_SORT
|
||||
sortFieldName = internalName
|
||||
}
|
||||
|
||||
const {processedData, sortedTimeVals} = processData(
|
||||
const {processedData, sortedTimeVals} = processTableData(
|
||||
data,
|
||||
sortFieldName,
|
||||
direction,
|
||||
|
@ -126,6 +103,11 @@ class TableGraph extends Component {
|
|||
? processedData[0]
|
||||
: processedData.map(row => row[0])
|
||||
|
||||
const labelsColumnWidth = calculateLabelsColumnWidth(
|
||||
processedLabels,
|
||||
fieldNames
|
||||
)
|
||||
|
||||
this.setState({
|
||||
data,
|
||||
labels,
|
||||
|
@ -133,10 +115,7 @@ class TableGraph extends Component {
|
|||
sortedTimeVals,
|
||||
sortField: sortFieldName,
|
||||
sortDirection: direction,
|
||||
labelsColumnWidth: calculateLabelsColumnWidth(
|
||||
processedLabels,
|
||||
fieldNames
|
||||
),
|
||||
labelsColumnWidth,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -150,7 +129,8 @@ class TableGraph extends Component {
|
|||
}
|
||||
|
||||
const firstDiff = Math.abs(hoverTime - sortedTimeVals[1]) // sortedTimeVals[0] is "time"
|
||||
const hoverTimeFound = sortedTimeVals.reduce(
|
||||
const hoverTimeFound = reduce(
|
||||
sortedTimeVals,
|
||||
(acc, currentTime, index) => {
|
||||
const thisDiff = Math.abs(hoverTime - currentTime)
|
||||
if (thisDiff < acc.diff) {
|
||||
|
@ -208,7 +188,7 @@ class TableGraph extends Component {
|
|||
direction = DEFAULT_SORT
|
||||
}
|
||||
|
||||
const {processedData, sortedTimeVals} = processData(
|
||||
const {processedData, sortedTimeVals} = processTableData(
|
||||
data,
|
||||
fieldName,
|
||||
direction,
|
||||
|
@ -379,6 +359,7 @@ class TableGraph extends Component {
|
|||
const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0)
|
||||
const tableHeight = _.get(this, ['gridContainer', 'clientHeight'], 0)
|
||||
const {scrollToColumn, scrollToRow} = this.calcScrollToColRow()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="table-graph-container"
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const YesNoButtons = ({onConfirm, onCancel, buttonSize}) => (
|
||||
<div>
|
||||
<button
|
||||
className={classnames('btn btn-square btn-info', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button
|
||||
className={classnames('btn btn-square btn-success', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, string} = PropTypes
|
||||
|
||||
YesNoButtons.propTypes = {
|
||||
onConfirm: func.isRequired,
|
||||
onCancel: func.isRequired,
|
||||
buttonSize: string,
|
||||
}
|
||||
YesNoButtons.defaultProps = {
|
||||
buttonSize: 'btn-sm',
|
||||
}
|
||||
|
||||
export default YesNoButtons
|
|
@ -5,9 +5,6 @@ export const NULL_ARRAY_INDEX = -1
|
|||
|
||||
export const NULL_HOVER_TIME = '0'
|
||||
|
||||
export const TIME_FORMAT_DEFAULT = 'MM/DD/YYYY HH:mm:ss.SS'
|
||||
export const TIME_FORMAT_CUSTOM = 'Custom'
|
||||
|
||||
export const TIME_FORMAT_TOOLTIP_LINK =
|
||||
'http://momentjs.com/docs/#/parsing/string-format/'
|
||||
|
||||
|
@ -19,20 +16,24 @@ export const TIME_FIELD_DEFAULT = {
|
|||
|
||||
export const ASCENDING = 'asc'
|
||||
export const DESCENDING = 'desc'
|
||||
export const DEFAULT_SORT = ASCENDING
|
||||
|
||||
export const FIX_FIRST_COLUMN_DEFAULT = true
|
||||
export const VERTICAL_TIME_AXIS_DEFAULT = true
|
||||
|
||||
export const CELL_HORIZONTAL_PADDING = 18
|
||||
|
||||
export const TIME_FORMAT_DEFAULT = 'MM/DD/YYYY HH:mm:ss'
|
||||
export const TIME_FORMAT_CUSTOM = 'Custom'
|
||||
|
||||
export const FORMAT_OPTIONS = [
|
||||
{text: TIME_FORMAT_DEFAULT},
|
||||
{text: 'MM/DD/YYYY HH:mm'},
|
||||
{text: 'MM/DD/YYYY'},
|
||||
{text: 'h:mm:ss A'},
|
||||
{text: 'h:mm A'},
|
||||
{text: 'MMMM D, YYYY'},
|
||||
{text: 'MMMM D, YYYY h:mm A'},
|
||||
{text: 'dddd, MMMM D, YYYY h:mm A'},
|
||||
{text: 'MM/DD/YYYY HH:mm:ss.SSS'},
|
||||
{text: 'YYYY-MM-DD HH:mm:ss'},
|
||||
{text: 'HH:mm:ss'},
|
||||
{text: 'HH:mm:ss.SSS'},
|
||||
{text: 'MMMM D, YYYY HH:mm:ss'},
|
||||
{text: 'dddd, MMMM D, YYYY HH:mm:ss'},
|
||||
{text: TIME_FORMAT_CUSTOM},
|
||||
]
|
||||
|
||||
|
@ -51,6 +52,7 @@ export const calculateTimeColumnWidth = timeFormat => {
|
|||
timeFormat = _.replace(timeFormat, 'dddd', 'Wednesday')
|
||||
timeFormat = _.replace(timeFormat, 'A', 'AM')
|
||||
timeFormat = _.replace(timeFormat, 'h', '00')
|
||||
timeFormat = _.replace(timeFormat, 'X', '1522286058')
|
||||
|
||||
const {width} = calculateSize(timeFormat, {
|
||||
font: '"RobotoMono", monospace',
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {Link} from 'react-router'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const {bool, node, string} = PropTypes
|
||||
|
||||
const NavListItem = React.createClass({
|
||||
propTypes: {
|
||||
link: string.isRequired,
|
||||
children: node,
|
||||
location: string,
|
||||
useAnchor: bool,
|
||||
isExternal: bool,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {link, children, location, useAnchor, isExternal} = this.props
|
||||
const isActive = location.startsWith(link)
|
||||
|
||||
return useAnchor ? (
|
||||
<a
|
||||
className={classnames('sidebar-menu--item', {active: isActive})}
|
||||
href={link}
|
||||
target={isExternal ? '_blank' : '_self'}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
className={classnames('sidebar-menu--item', {active: isActive})}
|
||||
to={link}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const NavHeader = React.createClass({
|
||||
propTypes: {
|
||||
link: string,
|
||||
title: string,
|
||||
useAnchor: bool,
|
||||
},
|
||||
render() {
|
||||
const {link, title, useAnchor} = this.props
|
||||
|
||||
// Some nav items, such as Logout, need to hit an external link rather
|
||||
// than simply route to an internal page. Anchor tags serve that purpose.
|
||||
return useAnchor ? (
|
||||
<a className="sidebar-menu--heading" href={link}>
|
||||
{title}
|
||||
</a>
|
||||
) : (
|
||||
<Link className="sidebar-menu--heading" to={link}>
|
||||
{title}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const NavBlock = React.createClass({
|
||||
propTypes: {
|
||||
children: node,
|
||||
link: string,
|
||||
icon: string.isRequired,
|
||||
location: string,
|
||||
className: string,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {location, className} = this.props
|
||||
const isActive = React.Children.toArray(this.props.children).find(child => {
|
||||
return location.startsWith(child.props.link) // if location is undefined, this will fail silently
|
||||
})
|
||||
|
||||
const children = React.Children.map(this.props.children, child => {
|
||||
if (child && child.type === NavListItem) {
|
||||
return React.cloneElement(child, {location})
|
||||
}
|
||||
|
||||
return child
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('sidebar--item', className, {active: isActive})}
|
||||
>
|
||||
{this.renderSquare()}
|
||||
<div className="sidebar-menu">
|
||||
{children}
|
||||
<div className="sidebar-menu--triangle" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderSquare() {
|
||||
const {link, icon} = this.props
|
||||
|
||||
if (!link) {
|
||||
return (
|
||||
<div className="sidebar--square">
|
||||
<div className={`sidebar--icon icon ${icon}`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="sidebar--square" to={link}>
|
||||
<div className={`sidebar--icon icon ${icon}`} />
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const NavBar = React.createClass({
|
||||
propTypes: {
|
||||
children: node,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {children} = this.props
|
||||
|
||||
return <nav className="sidebar">{children}</nav>
|
||||
},
|
||||
})
|
||||
|
||||
export {NavBar, NavBlock, NavHeader, NavListItem}
|
|
@ -0,0 +1,120 @@
|
|||
import React, {PureComponent, SFC, ReactNode, ReactElement} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import classnames from 'classnames'
|
||||
|
||||
interface NavListItemProps {
|
||||
link: string
|
||||
location?: string
|
||||
useAnchor?: boolean
|
||||
isExternal?: boolean
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const NavListItem: SFC<NavListItemProps> = ({
|
||||
link,
|
||||
children,
|
||||
location,
|
||||
useAnchor,
|
||||
isExternal,
|
||||
}) => {
|
||||
const isActive = location.startsWith(link)
|
||||
return useAnchor ? (
|
||||
<a
|
||||
className={classnames('sidebar-menu--item', {active: isActive})}
|
||||
href={link}
|
||||
target={isExternal ? '_blank' : '_self'}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
className={classnames('sidebar-menu--item', {active: isActive})}
|
||||
to={link}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavHeaderProps {
|
||||
link?: string
|
||||
title?: string
|
||||
useAnchor?: string
|
||||
}
|
||||
|
||||
const NavHeader: SFC<NavHeaderProps> = ({link, title, useAnchor}) => {
|
||||
// Some nav items, such as Logout, need to hit an external link rather
|
||||
// than simply route to an internal page. Anchor tags serve that purpose.
|
||||
return useAnchor ? (
|
||||
<a className="sidebar-menu--heading" href={link}>
|
||||
{title}
|
||||
</a>
|
||||
) : (
|
||||
<Link className="sidebar-menu--heading" to={link}>
|
||||
{title}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavBlockProps {
|
||||
children?: ReactNode
|
||||
link?: string
|
||||
icon: string
|
||||
location?: string
|
||||
className?: string
|
||||
matcher?: string
|
||||
}
|
||||
|
||||
class NavBlock extends PureComponent<NavBlockProps> {
|
||||
public render() {
|
||||
const {location, className} = this.props
|
||||
const isActive = React.Children.toArray(this.props.children).find(
|
||||
(child: ReactElement<any>) => {
|
||||
return location.startsWith(child.props.link) // if location is undefined, this will fail silently
|
||||
}
|
||||
)
|
||||
|
||||
const children = React.Children.map(
|
||||
this.props.children,
|
||||
(child: ReactElement<any>) => {
|
||||
if (child && child.type === NavListItem) {
|
||||
return React.cloneElement(child, {location})
|
||||
}
|
||||
|
||||
return child
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('sidebar--item', className, {active: isActive})}
|
||||
>
|
||||
{this.renderSquare()}
|
||||
<div className="sidebar-menu">
|
||||
{children}
|
||||
<div className="sidebar-menu--triangle" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderSquare() {
|
||||
const {link, icon} = this.props
|
||||
|
||||
if (!link) {
|
||||
return (
|
||||
<div className="sidebar--square">
|
||||
<div className={`sidebar--icon icon ${icon}`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="sidebar--square" to={link}>
|
||||
<div className={`sidebar--icon icon ${icon}`} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export {NavBlock, NavHeader, NavListItem}
|
|
@ -1,66 +1,37 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent} from 'react'
|
||||
import {withRouter, Link} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import Authorized, {ADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import UserNavBlock from 'src/side_nav/components/UserNavBlock'
|
||||
import FeatureFlag from 'src/shared/components/FeatureFlag'
|
||||
|
||||
import {
|
||||
NavBar,
|
||||
NavBlock,
|
||||
NavHeader,
|
||||
NavListItem,
|
||||
} from 'src/side_nav/components/NavItems'
|
||||
|
||||
import {DEFAULT_HOME_PAGE} from 'shared/constants'
|
||||
import {DEFAULT_HOME_PAGE} from 'src/shared/constants'
|
||||
import {Params, Location, Links, Me} from 'src/types/sideNav'
|
||||
|
||||
const {arrayOf, bool, shape, string} = PropTypes
|
||||
interface Props {
|
||||
params: Params
|
||||
location: Location
|
||||
isHidden: boolean
|
||||
isUsingAuth?: boolean
|
||||
logoutLink?: string
|
||||
links?: Links
|
||||
me: Me
|
||||
}
|
||||
|
||||
const SideNav = React.createClass({
|
||||
propTypes: {
|
||||
params: shape({
|
||||
sourceID: string.isRequired,
|
||||
}).isRequired,
|
||||
location: shape({
|
||||
pathname: string.isRequired,
|
||||
}).isRequired,
|
||||
isHidden: bool.isRequired,
|
||||
isUsingAuth: bool,
|
||||
logoutLink: string,
|
||||
links: shape({
|
||||
me: string,
|
||||
external: shape({
|
||||
custom: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
url: string.isRequired,
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
me: shape({
|
||||
currentOrganization: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}),
|
||||
name: string,
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
})
|
||||
),
|
||||
roles: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
name: string,
|
||||
})
|
||||
),
|
||||
}),
|
||||
},
|
||||
class SideNav extends PureComponent<Props> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const {
|
||||
params: {sourceID},
|
||||
location: {pathname: location},
|
||||
|
@ -77,7 +48,7 @@ const SideNav = React.createClass({
|
|||
const isDefaultPage = location.split('/').includes(DEFAULT_HOME_PAGE)
|
||||
|
||||
return isHidden ? null : (
|
||||
<NavBar location={location}>
|
||||
<nav className="sidebar">
|
||||
<div
|
||||
className={isDefaultPage ? 'sidebar--item active' : 'sidebar--item'}
|
||||
>
|
||||
|
@ -103,7 +74,7 @@ const SideNav = React.createClass({
|
|||
link={`${sourcePrefix}/dashboards`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/dashboards`} title={'Dashboards'} />
|
||||
<NavHeader link={`${sourcePrefix}/dashboards`} title="Dashboards" />
|
||||
</NavBlock>
|
||||
<NavBlock
|
||||
matcher="alerts"
|
||||
|
@ -170,10 +141,20 @@ const SideNav = React.createClass({
|
|||
sourcePrefix={sourcePrefix}
|
||||
/>
|
||||
) : null}
|
||||
</NavBar>
|
||||
<FeatureFlag name="time-machine">
|
||||
<NavBlock
|
||||
icon="cog-thick"
|
||||
link={`${sourcePrefix}/ifql`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/ifql`} title="IFQL Builder" />
|
||||
</NavBlock>
|
||||
</FeatureFlag>
|
||||
</nav>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
auth: {isUsingAuth, logoutLink, me},
|
||||
app: {ephemeral: {inPresentationMode}},
|
|
@ -1,91 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const {func} = PropTypes
|
||||
const RenameCluster = React.createClass({
|
||||
propTypes: {
|
||||
onRenameCluster: func.isRequired,
|
||||
},
|
||||
|
||||
handleRenameCluster(e) {
|
||||
e.preventDefault()
|
||||
this.props.onRenameCluster(this._displayName.value)
|
||||
this._displayName.value = ''
|
||||
|
||||
$('.modal').hide() // eslint-disable-line no-undef
|
||||
$('.modal-backdrop').hide() // eslint-disable-line no-undef
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className="modal fade"
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
id="rename-cluster-modal"
|
||||
>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button
|
||||
type="button"
|
||||
className="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 className="modal-title">Rename Cluster</h4>
|
||||
</div>
|
||||
<form onSubmit={this.handleRenameCluster}>
|
||||
<div className="modal-body">
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2 text-center">
|
||||
<p>
|
||||
A cluster can have an alias that replaces its ID in the
|
||||
interface.
|
||||
<br />
|
||||
This does not affect the cluster ID.
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label htmlFor="cluster-alias" className="sr-only">
|
||||
Cluster Alias
|
||||
</label>
|
||||
<input
|
||||
ref={ref => (this._displayName = ref)}
|
||||
required={true}
|
||||
type="text"
|
||||
className="input-lg form-control"
|
||||
maxLength="22"
|
||||
id="cluster-alias"
|
||||
placeholder="Name this cluster..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-default"
|
||||
data-dismiss="modal"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
disabled={!this._displayName}
|
||||
type="submit"
|
||||
className="btn btn-primary js-rename-cluster"
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default RenameCluster
|
|
@ -7,6 +7,7 @@ import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
|||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
|
||||
const kapacitorDropdown = (
|
||||
kapacitors,
|
||||
|
@ -96,117 +97,124 @@ const InfluxTable = ({
|
|||
handleDeleteKapacitor,
|
||||
isUsingAuth,
|
||||
me,
|
||||
}) => (
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">
|
||||
{isUsingAuth ? (
|
||||
<span>
|
||||
Connections for <em>{me.currentOrganization.name}</em>
|
||||
</span>
|
||||
) : (
|
||||
<span>Connections</span>
|
||||
)}
|
||||
</h2>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<Link
|
||||
to={`/sources/${source.id}/manage-sources/new`}
|
||||
className="btn btn-sm btn-primary"
|
||||
>
|
||||
<span className="icon plus" /> Add Connection
|
||||
</Link>
|
||||
</Authorized>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<table className="table v-center margin-bottom-zero table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="source-table--connect-col" />
|
||||
<th>InfluxDB Connection</th>
|
||||
<th className="text-right" />
|
||||
<th>
|
||||
Kapacitor Connection{' '}
|
||||
<QuestionMarkTooltip
|
||||
tipID="kapacitor-node-helper"
|
||||
tipContent={
|
||||
'<p>Kapacitor Connections are<br/>scoped per InfluxDB Connection.<br/>Only one can be active at a time.</p>'
|
||||
}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sources.map(s => {
|
||||
return (
|
||||
<tr
|
||||
key={s.id}
|
||||
className={s.id === source.id ? 'highlight' : null}
|
||||
>
|
||||
<td>
|
||||
{s.id === source.id ? (
|
||||
<div className="btn btn-success btn-xs source-table--connect">
|
||||
Connected
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
className="btn btn-default btn-xs source-table--connect"
|
||||
to={`/sources/${s.id}/hosts`}
|
||||
>
|
||||
Connect
|
||||
</Link>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<h5 className="margin-zero">
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWithIfNotAuthorized={<strong>{s.name}</strong>}
|
||||
>
|
||||
}) => {
|
||||
const wrappedDelete = s => () => {
|
||||
handleDeleteSource(s)
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">
|
||||
{isUsingAuth ? (
|
||||
<span>
|
||||
Connections for <em>{me.currentOrganization.name}</em>
|
||||
</span>
|
||||
) : (
|
||||
<span>Connections</span>
|
||||
)}
|
||||
</h2>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<Link
|
||||
to={`/sources/${source.id}/manage-sources/new`}
|
||||
className="btn btn-sm btn-primary"
|
||||
>
|
||||
<span className="icon plus" /> Add Connection
|
||||
</Link>
|
||||
</Authorized>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<table className="table v-center margin-bottom-zero table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="source-table--connect-col" />
|
||||
<th>InfluxDB Connection</th>
|
||||
<th className="text-right" />
|
||||
<th>
|
||||
Kapacitor Connection{' '}
|
||||
<QuestionMarkTooltip
|
||||
tipID="kapacitor-node-helper"
|
||||
tipContent={
|
||||
'<p>Kapacitor Connections are<br/>scoped per InfluxDB Connection.<br/>Only one can be active at a time.</p>'
|
||||
}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sources.map(s => {
|
||||
return (
|
||||
<tr
|
||||
key={s.id}
|
||||
className={s.id === source.id ? 'highlight' : null}
|
||||
>
|
||||
<td>
|
||||
{s.id === source.id ? (
|
||||
<div className="btn btn-success btn-xs source-table--connect">
|
||||
Connected
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to={`${location.pathname}/${s.id}/edit`}
|
||||
className={
|
||||
s.id === source.id ? 'link-success' : null
|
||||
className="btn btn-default btn-xs source-table--connect"
|
||||
to={`/sources/${s.id}/hosts`}
|
||||
>
|
||||
Connect
|
||||
</Link>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<h5 className="margin-zero">
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWithIfNotAuthorized={
|
||||
<strong>{s.name}</strong>
|
||||
}
|
||||
>
|
||||
<strong>{s.name}</strong>
|
||||
{s.default ? ' (Default)' : null}
|
||||
</Link>
|
||||
<Link
|
||||
to={`${location.pathname}/${s.id}/edit`}
|
||||
className={
|
||||
s.id === source.id ? 'link-success' : null
|
||||
}
|
||||
>
|
||||
<strong>{s.name}</strong>
|
||||
{s.default ? ' (Default)' : null}
|
||||
</Link>
|
||||
</Authorized>
|
||||
</h5>
|
||||
<span>{s.url}</span>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<ConfirmButton
|
||||
customClass="delete-source table--show-on-row-hover"
|
||||
type="btn-danger"
|
||||
size="btn-xs"
|
||||
text="Delete Connection"
|
||||
confirmAction={wrappedDelete(s)}
|
||||
/>
|
||||
</Authorized>
|
||||
</h5>
|
||||
<span>{s.url}</span>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<a
|
||||
className="btn btn-xs btn-danger table--show-on-row-hover"
|
||||
href="#"
|
||||
onClick={handleDeleteSource(s)}
|
||||
>
|
||||
Delete Connection
|
||||
</a>
|
||||
</Authorized>
|
||||
</td>
|
||||
<td className="source-table--kapacitor">
|
||||
{kapacitorDropdown(
|
||||
s.kapacitors,
|
||||
s,
|
||||
router,
|
||||
setActiveKapacitor,
|
||||
handleDeleteKapacitor
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td className="source-table--kapacitor">
|
||||
{kapacitorDropdown(
|
||||
s.kapacitors,
|
||||
s,
|
||||
router,
|
||||
setActiveKapacitor,
|
||||
handleDeleteKapacitor
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const {array, bool, func, shape, string} = PropTypes
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import AJAX from 'utils/ajax'
|
||||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
const excludeBasepath = true // don't prefix route of external link with basepath/
|
||||
|
||||
export const fetchJSONFeed = url =>
|
||||
AJAX(
|
||||
|
@ -9,5 +11,5 @@ export const fetchJSONFeed = url =>
|
|||
// https://stackoverflow.com/questions/22968406/how-to-skip-the-options-preflight-request-in-angularjs
|
||||
headers: {'Content-Type': 'text/plain; charset=UTF-8'},
|
||||
},
|
||||
{excludeBasepath: true} // don't prefix route of external link with basepath
|
||||
excludeBasepath // don't prefix route of external link with basepath
|
||||
)
|
|
@ -35,11 +35,9 @@
|
|||
@import 'components/crosshairs';
|
||||
@import 'components/ceo-display-options';
|
||||
@import 'components/confirm-button';
|
||||
@import 'components/confirm-buttons';
|
||||
@import 'components/confirm-or-cancel';
|
||||
@import 'components/code-mirror-theme';
|
||||
@import 'components/color-dropdown';
|
||||
@import 'components/confirm-button';
|
||||
@import 'components/confirm-buttons';
|
||||
@import 'components/custom-time-range';
|
||||
@import 'components/customize-fields';
|
||||
@import 'components/dygraphs';
|
||||
|
@ -71,6 +69,7 @@
|
|||
@import 'components/table-graph';
|
||||
@import 'components/threshold-controls';
|
||||
@import 'components/kapacitor-logs-table';
|
||||
@import 'components/func-node.scss';
|
||||
|
||||
// Pages
|
||||
@import 'pages/config-endpoints';
|
||||
|
@ -85,3 +84,6 @@
|
|||
|
||||
// TODO
|
||||
@import 'unsorted';
|
||||
|
||||
// IFQL - Time Machine
|
||||
@import 'components/funcs-button';
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
Confirmation Buttons
|
||||
------------------------------------------------------
|
||||
*/
|
||||
.confirm-buttons {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
& > * {
|
||||
margin: 0 0 0 4px !important;
|
||||
|
||||
&:first-child {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
"Confirm or Cancel" Buttons
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.confirm-or-cancel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.confirm-or-cancel--confirm {
|
||||
order: 2;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.confirm-or-cancel--cancel {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
&.reversed {
|
||||
.confirm-or-cancel--confirm {
|
||||
order: 1;
|
||||
margin-left: 0;
|
||||
}
|
||||
.confirm-or-cancel--cancel {
|
||||
order: 2;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,8 @@ $fancytable--table--margin: 4px;
|
|||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
> div:not(.confirm-buttons) {
|
||||
> div:not(.confirm-or-cancel),
|
||||
> div:not(.confirm-button) {
|
||||
margin-right: $fancytable--table--margin;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
.func-node-container {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.func-node {
|
||||
background: #252b35;
|
||||
border-radius: $radius-small;
|
||||
padding: 10px;
|
||||
width: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: $ix-text-default;
|
||||
text-transform: uppercase;
|
||||
margin: $ix-marg-a;
|
||||
font-family: $ix-text-font;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.dropdown-item.func {
|
||||
text-transform: uppercase;
|
||||
}
|
|
@ -26,10 +26,12 @@ $orgs-table--delete-width: 30px;
|
|||
width: $orgs-table--active-width;
|
||||
justify-content: center;
|
||||
@include no-user-select();
|
||||
|
||||
.btn {width: 100%;}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.orgs-table--default-role.deleting {
|
||||
.orgs-table--default-role.creating {
|
||||
width: (
|
||||
$orgs-table--default-role-width - $fancytable--table--margin -
|
||||
$orgs-table--delete-width
|
||||
|
|
|
@ -28,22 +28,35 @@ $tvmp-table-gutter: 8px;
|
|||
}
|
||||
}
|
||||
.template-variable-manager--body {
|
||||
padding: 18px ($tvmp-gutter - $tvmp-table-gutter) $tvmp-gutter ($tvmp-gutter - $tvmp-table-gutter);
|
||||
padding: 18px ($tvmp-gutter - $tvmp-table-gutter) $tvmp-gutter
|
||||
($tvmp-gutter - $tvmp-table-gutter);
|
||||
border-radius: 0 0 $radius $radius;
|
||||
min-height: $tvmp-min-height;
|
||||
max-height: $tvmp-max-height;
|
||||
@include gradient-v($g2-kevlar,$g0-obsidian);
|
||||
@include custom-scrollbar-round($g0-obsidian,$g3-castle);
|
||||
@include gradient-v($g2-kevlar, $g0-obsidian);
|
||||
@include custom-scrollbar-round($g0-obsidian, $g3-castle);
|
||||
}
|
||||
.template-variable-manager--table,
|
||||
.template-variable-manager--table-container {
|
||||
width: 100%;
|
||||
}
|
||||
/* Column Widths */
|
||||
.tvm--col-1 {flex: 0 0 140px;}
|
||||
.tvm--col-2 {flex: 0 0 140px;}
|
||||
.tvm--col-3 {flex: 1 0 0;}
|
||||
.tvm--col-4 {flex: 0 0 68px;}
|
||||
.tvm--col-1 {
|
||||
flex: 0 0 140px;
|
||||
}
|
||||
.tvm--col-2 {
|
||||
flex: 0 0 140px;
|
||||
}
|
||||
.tvm--col-3 {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.tvm--col-4 {
|
||||
flex: 0 0 30px;
|
||||
|
||||
&.editing {
|
||||
flex: 0 0 68px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table Column Labels */
|
||||
.template-variable-manager--table-heading {
|
||||
|
@ -61,7 +74,9 @@ $tvmp-table-gutter: 8px;
|
|||
@include no-user-select();
|
||||
padding-left: 6px;
|
||||
margin-right: $tvmp-table-gutter;
|
||||
&:last-child {margin-right: 0;}
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +99,9 @@ $tvmp-table-gutter: 8px;
|
|||
|
||||
> * {
|
||||
margin-right: $tvmp-table-gutter;
|
||||
&:last-child {margin-right: 0;}
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,8 +127,7 @@ $tvmp-table-gutter: 8px;
|
|||
border: 2px solid $g5-pepper;
|
||||
background-color: $g2-kevlar;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.25s ease;
|
||||
transition: border-color 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
cursor: text;
|
||||
|
@ -167,12 +183,16 @@ $tvmp-table-gutter: 8px;
|
|||
margin-bottom: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
> *:last-child {margin-right: 0;}
|
||||
> *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
|
||||
& > .dropdown-toggle {width: 100%;}
|
||||
& > .dropdown-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tvm-query-builder--text {
|
||||
|
@ -188,29 +208,13 @@ $tvmp-table-gutter: 8px;
|
|||
font-weight: 600;
|
||||
font-family: $code-font;
|
||||
}
|
||||
|
||||
.tvm-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.btn-edit {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
> .btn:first-child {
|
||||
& > .btn:nth-child(2) {
|
||||
margin-left: $tvmp-table-gutter;
|
||||
}
|
||||
|
||||
/* Override confirm buttons styles */
|
||||
/* Janky, but doing this quick & dirty for now */
|
||||
.btn-danger {
|
||||
order: 2;
|
||||
}
|
||||
.confirm-buttons > .btn {
|
||||
margin-left: $tvmp-table-gutter !important;
|
||||
}
|
||||
/* Hide the edit button when confirming a delete */
|
||||
.confirm-buttons + .btn-edit {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue