mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
resolving conflicts upon merge with upstream master
This commit is contained in:
commit
314044830e
170 changed files with 4771 additions and 2902 deletions
14
TODOS.md
14
TODOS.md
|
@ -4,22 +4,12 @@
|
|||
- **src/kibana/apps/dashboard/directives/grid.js**
|
||||
- change this from event based to calling a method on dashboardApp – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/dashboard/directives/grid.js)
|
||||
- **src/kibana/apps/discover/controllers/discover.js**
|
||||
- Switch this to watching time.string when we implement it – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
|
||||
- On array fields, negating does not negate the combination, rather all terms – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
|
||||
- Move to utility class – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
|
||||
- Move to utility class – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
|
||||
- a legit way to update the index pattern – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
|
||||
- **src/kibana/apps/settings/sections/indices/_create.js**
|
||||
- we should probably display a message of some kind – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/settings/sections/indices/_create.js)
|
||||
- **src/kibana/apps/visualize/controllers/editor.js**
|
||||
- Switch this to watching time.string when we implement it – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/controllers/editor.js)
|
||||
- **src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js**
|
||||
- Should we abtract out the agg building stuff? – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js)
|
||||
- Should this be abstracted somewhere? Its a copy/paste from _saved_vis.js – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js)
|
||||
- **src/kibana/apps/visualize/saved_visualizations/_type_defs.js**
|
||||
- We need to be able to get ahold of angular services here – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_type_defs.js)
|
||||
- We need to be able to get ahold of angular services here – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_type_defs.js)
|
||||
- **src/kibana/apps/visualize/saved_visualizations/bucket_aggs/terms.js**
|
||||
- We need more just _count here. – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/terms.js)
|
||||
- **src/kibana/components/agg_types/buckets/terms.js**
|
||||
- We need more than just _count here. – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/agg_types/buckets/terms.js)
|
||||
- **src/kibana/components/index_patterns/_mapper.js**
|
||||
|
@ -30,8 +20,6 @@
|
|||
- we need to have some way to clean up result requests – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/visualize/visualize.js)
|
||||
- **src/kibana/directives/rows.js**
|
||||
- It would be better to actually check the type of the field, but we don't have – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/directives/rows.js)
|
||||
- **src/kibana/services/timefilter.js**
|
||||
- This should be disabled on route change, apps need to enable it explicitly – (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/services/timefilter.js)
|
||||
- **test/unit/specs/apps/dashboard/directives/panel.js**
|
||||
- This should not be needed, timefilter is only included here – (https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/apps/dashboard/directives/panel.js)
|
||||
- **test/unit/specs/directives/timepicker.js**
|
||||
|
|
10
server/Gemfile
Normal file
10
server/Gemfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
ruby "1.9.3", :engine => 'jruby', :engine_version => '1.7.13'
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem 'sinatra', :require => 'sinatra/base'
|
||||
gem 'sinatra-contrib'
|
||||
gem 'puma'
|
||||
gem 'warbler'
|
||||
gem 'elasticsearch'
|
||||
gem 'rack-reverse-proxy', :require => 'rack/reverse_proxy'
|
60
server/Gemfile.lock
Normal file
60
server/Gemfile.lock
Normal file
|
@ -0,0 +1,60 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
backports (3.6.0)
|
||||
elasticsearch (1.0.4)
|
||||
elasticsearch-api (= 1.0.4)
|
||||
elasticsearch-transport (= 1.0.4)
|
||||
elasticsearch-api (1.0.4)
|
||||
multi_json
|
||||
elasticsearch-transport (1.0.4)
|
||||
faraday
|
||||
multi_json
|
||||
faraday (0.9.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
jruby-jars (1.7.13)
|
||||
jruby-rack (1.1.16)
|
||||
multi_json (1.10.1)
|
||||
multipart-post (2.0.0)
|
||||
puma (2.9.0)
|
||||
rack (>= 1.1, < 2.0)
|
||||
puma (2.9.0-java)
|
||||
rack (>= 1.1, < 2.0)
|
||||
rack (1.5.2)
|
||||
rack-protection (1.5.3)
|
||||
rack
|
||||
rack-reverse-proxy (0.4.4)
|
||||
rack (>= 1.0.0)
|
||||
rack-test (0.6.2)
|
||||
rack (>= 1.0)
|
||||
rake (10.1.0)
|
||||
rubyzip (1.1.6)
|
||||
sinatra (1.4.5)
|
||||
rack (~> 1.4)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (~> 1.3, >= 1.3.4)
|
||||
sinatra-contrib (1.4.2)
|
||||
backports (>= 2.0)
|
||||
multi_json
|
||||
rack-protection
|
||||
rack-test
|
||||
sinatra (~> 1.4.0)
|
||||
tilt (~> 1.3)
|
||||
tilt (1.4.1)
|
||||
warbler (1.4.4)
|
||||
jruby-jars (>= 1.5.6, < 2.0)
|
||||
jruby-rack (>= 1.0.0)
|
||||
rake (>= 0.9.6)
|
||||
rubyzip (>= 0.9, < 1.2)
|
||||
|
||||
PLATFORMS
|
||||
java
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
elasticsearch
|
||||
puma
|
||||
rack-reverse-proxy
|
||||
sinatra
|
||||
sinatra-contrib
|
||||
warbler
|
79
server/README.md
Normal file
79
server/README.md
Normal file
|
@ -0,0 +1,79 @@
|
|||
# Backend Server For Kibana 4
|
||||
|
||||
This is the backend server for Kibana 4. It's written in Ruby using Sinatra and the Puma rack server. It's written to be compatible with JRuby and distributed as a jar file.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Serve Static Files for Kibana
|
||||
- Proxy requests to Elasticsearch
|
||||
- Distributable an executable jar
|
||||
- Platform for developing API endpoints for Kibana
|
||||
|
||||
### Configuration
|
||||
|
||||
Coming Soon...
|
||||
|
||||
### Project Layout
|
||||
- **bin** - Where the kibana executable lives. This is the entry point for the Jar file
|
||||
- **config** - Configuration files for warble, puma and the rack server
|
||||
- **lib** - Where Kibana specific ruby libraries live
|
||||
- **public** - This is where the static assets go
|
||||
- **routes** - This is where the route controllers go
|
||||
- **Gemfile** - This is the Gemfile for Bundler. Any dependencies need to be listed in here.
|
||||
- **Gemfile.lock** - Bundler creates a lock file for the gem versions
|
||||
- **Rakefile** - This is where the `rake` tasks go
|
||||
- **README.md** - You're looking at it :D
|
||||
|
||||
### Development Evn Setup
|
||||
|
||||
Coming Soon...
|
||||
|
||||
### Build Process
|
||||
|
||||
Coming Soon...
|
||||
|
||||
### Adding New Routes to the Project
|
||||
|
||||
Create a route class in `routes`
|
||||
|
||||
```ruby
|
||||
require "rotues/base"
|
||||
|
||||
module Kibana
|
||||
module Routes
|
||||
class MyNewRoute < Base
|
||||
|
||||
get '/my-route/' do
|
||||
json :something => 'fancy'
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Require the route in `lib/app` at the top of the file
|
||||
|
||||
```ruby
|
||||
require "routes/home"
|
||||
require "routes/api"
|
||||
require "routes/my_new_route"
|
||||
|
||||
```
|
||||
|
||||
Now add the route to the class
|
||||
|
||||
```ruby
|
||||
# Rack middleware goes here
|
||||
use Rack::ReverseProxy do
|
||||
reverse_proxy /^\/elasticsearch(.*)$/, 'http://localhost:9200$1'
|
||||
end
|
||||
|
||||
# Routes go here
|
||||
use Routes::Home
|
||||
use Routes::Api
|
||||
use Routes::MyNewRoute
|
||||
end
|
||||
end
|
||||
|
||||
```
|
30
server/Rakefile
Normal file
30
server/Rakefile
Normal file
|
@ -0,0 +1,30 @@
|
|||
require "java"
|
||||
require "warbler"
|
||||
|
||||
HERE = File.expand_path(File.dirname(__FILE__))
|
||||
|
||||
task "default" => "jar:run"
|
||||
|
||||
namespace "jar" do
|
||||
desc "Run the project jar file"
|
||||
task "run" => "jar" do
|
||||
exec("cd #{HERE} && rm -rf /tmp/kibana* && cp kibana.jar /tmp && cd /tmp && unzip -o kibana.jar 'kibana/public/*' && unzip -o kibana.jar kibana/config/web.ru && env PUBLIC_FOLDER=/tmp/kibana/public java -server -jar kibana.jar kibana/config/web.ru")
|
||||
end
|
||||
end
|
||||
|
||||
desc "Create the project jar file"
|
||||
task "jar" do
|
||||
system("cd #{HERE} && jruby -S warble")
|
||||
end
|
||||
|
||||
# desc "Watch for changes"
|
||||
# task "watch" => "vendor/fswatch" do
|
||||
# system("killall fswatch")
|
||||
# system("fswatch #{HERE}/lib \"bash -c \\\"kill -SIGUSR2 \\\`ps u|grep [o]rg.jruby.Main|grep bin/puma|awk {\'print \\\$2\'}\\\`\\\"\" &")
|
||||
# end
|
||||
|
||||
desc "Run the project from jruby"
|
||||
# task "run" => "watch" do
|
||||
task "run" do
|
||||
exec("cd #{HERE} && jruby -S bundle exec jruby -S bin/kibana config/web.ru")
|
||||
end
|
38
server/bin/kibana
Executable file
38
server/bin/kibana
Executable file
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env ruby
|
||||
#
|
||||
# This file was generated by RubyGems.
|
||||
#
|
||||
# The application 'puma' is installed as part of a gem, and
|
||||
# this file is here to facilitate running it.
|
||||
#
|
||||
|
||||
require 'rubygems'
|
||||
|
||||
version = ">= 0"
|
||||
|
||||
HERE = File.expand_path(File.dirname(__FILE__))
|
||||
|
||||
if ARGV.first
|
||||
str = ARGV.first
|
||||
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||
if str =~ /\A_(.*)_\z/
|
||||
version = $1
|
||||
ARGV.shift
|
||||
end
|
||||
end
|
||||
|
||||
# Include the puma config unless it's been overriden
|
||||
unless ARGV.include?('-C')
|
||||
ARGV << '-C'
|
||||
ARGV << "#{HERE}/../config/puma.rb"
|
||||
end
|
||||
|
||||
# Include the rack config if it hasn't been included
|
||||
if (ARGV.grep(/config\/web\.ru/)).empty?
|
||||
ARGV << "#{HERE}/../config/web.ru"
|
||||
end
|
||||
|
||||
print ARGV, "\n"
|
||||
|
||||
gem 'puma', version
|
||||
load Gem.bin_path('puma', 'puma', version)
|
2
server/config/puma.rb
Normal file
2
server/config/puma.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
port 8000
|
||||
|
178
server/config/warble.rb
Normal file
178
server/config/warble.rb
Normal file
|
@ -0,0 +1,178 @@
|
|||
# Disable Rake-environment-task framework detection by uncommenting/setting to false
|
||||
# Warbler.framework_detection = false
|
||||
|
||||
# Warbler web application assembly configuration file
|
||||
Warbler::Config.new do |config|
|
||||
# Features: additional options controlling how the jar is built.
|
||||
# Currently the following features are supported:
|
||||
# - gemjar: package the gem repository in a jar file in WEB-INF/lib
|
||||
# - executable: embed a web server and make the war executable
|
||||
# - compiled: compile .rb files to .class files
|
||||
# config.features = %w(gemjar)
|
||||
|
||||
# Application directories to be included in the webapp.
|
||||
config.dirs = %w(bin config routes lib public)
|
||||
|
||||
# Additional files/directories to include, above those in config.dirs
|
||||
# config.includes = FileList["db"]
|
||||
|
||||
# Additional files/directories to exclude
|
||||
# config.excludes = FileList["lib/tasks/*"]
|
||||
|
||||
# Additional Java .jar files to include. Note that if .jar files are placed
|
||||
# in lib (and not otherwise excluded) then they need not be mentioned here.
|
||||
# JRuby and JRuby-Rack are pre-loaded in this list. Be sure to include your
|
||||
# own versions if you directly set the value
|
||||
# config.java_libs += FileList["lib/java/*.jar"]
|
||||
|
||||
# Loose Java classes and miscellaneous files to be included.
|
||||
# config.java_classes = FileList["target/classes/**.*"]
|
||||
|
||||
# One or more pathmaps defining how the java classes should be copied into
|
||||
# the archive. The example pathmap below accompanies the java_classes
|
||||
# configuration above. See http://rake.rubyforge.org/classes/String.html#M000017
|
||||
# for details of how to specify a pathmap.
|
||||
# config.pathmaps.java_classes << "%{target/classes/,}p"
|
||||
|
||||
# Bundler support is built-in. If Warbler finds a Gemfile in the
|
||||
# project directory, it will be used to collect the gems to bundle
|
||||
# in your application. If you wish to explicitly disable this
|
||||
# functionality, uncomment here.
|
||||
# config.bundler = false
|
||||
|
||||
# An array of Bundler groups to avoid including in the war file.
|
||||
# Defaults to ["development", "test", "assets"].
|
||||
# config.bundle_without = []
|
||||
|
||||
# Other gems to be included. If you don't use Bundler or a gemspec
|
||||
# file, you need to tell Warbler which gems your application needs
|
||||
# so that they can be packaged in the archive.
|
||||
# For Rails applications, the Rails gems are included by default
|
||||
# unless the vendor/rails directory is present.
|
||||
# config.gems += ["activerecord-jdbcmysql-adapter", "jruby-openssl"]
|
||||
# config.gems << "tzinfo"
|
||||
# config.gems << "sinatra"
|
||||
|
||||
# Uncomment this if you don't want to package rails gem.
|
||||
# config.gems -= ["rails"]
|
||||
|
||||
# The most recent versions of gems are used.
|
||||
# You can specify versions of gems by using a hash assignment:
|
||||
# config.gems["rails"] = "2.3.10"
|
||||
|
||||
# You can also use regexps or Gem::Dependency objects for flexibility or
|
||||
# finer-grained control.
|
||||
# config.gems << /^merb-/
|
||||
# config.gems << Gem::Dependency.new("merb-core", "= 0.9.3")
|
||||
|
||||
# Include gem dependencies not mentioned specifically. Default is
|
||||
# true, uncomment to turn off.
|
||||
# config.gem_dependencies = false
|
||||
|
||||
# Array of regular expressions matching relative paths in gems to be
|
||||
# excluded from the war. Defaults to empty, but you can set it like
|
||||
# below, which excludes test files.
|
||||
# config.gem_excludes = [/^(test|spec)\//]
|
||||
|
||||
# Pathmaps for controlling how application files are copied into the archive
|
||||
# config.pathmaps.application = ["WEB-INF/%p"]
|
||||
|
||||
# Name of the archive (without the extension). Defaults to the basename
|
||||
# of the project directory.
|
||||
config.jar_name = "kibana"
|
||||
|
||||
# Name of the MANIFEST.MF template for the war file. Defaults to a simple
|
||||
# MANIFEST.MF that contains the version of Warbler used to create the war file.
|
||||
# config.manifest_file = "config/MANIFEST.MF"
|
||||
|
||||
# When using the 'compiled' feature and specified, only these Ruby
|
||||
# files will be compiled. Default is to compile all \.rb files in
|
||||
# the application.
|
||||
# config.compiled_ruby_files = FileList['app/**/*.rb']
|
||||
|
||||
# Determines if ruby files in supporting gems will be compiled.
|
||||
# Ignored unless compile feature is used.
|
||||
# config.compile_gems = false
|
||||
|
||||
# When set it specify the bytecode version for compiled class files
|
||||
# config.bytecode_version = "1.6"
|
||||
|
||||
# When set to true, Warbler will override the value of ENV['GEM_HOME'] even it
|
||||
# has already been set. When set to false it will use any existing value of
|
||||
# GEM_HOME if it is set.
|
||||
# config.override_gem_home = true
|
||||
|
||||
# Allows for specifing custom executables
|
||||
# config.executable = ["rake", "bin/rake"]
|
||||
|
||||
# Sets default (prefixed) parameters for the executables
|
||||
# config.executable_params = "do:something"
|
||||
|
||||
# If set to true, moves jar files into WEB-INF/lib. Prior to version 1.4.2 of Warbler this was done
|
||||
# by default. But since 1.4.2 this config defaults to false. It may need to be set to true for
|
||||
# web servers that do not explode the WAR file.
|
||||
# Alternatively, this option can be set to a regular expression, which will
|
||||
# act as a jar selector -- only jar files that match the pattern will be
|
||||
# included in the archive.
|
||||
# config.move_jars_to_webinf_lib = false
|
||||
|
||||
# === War files only below here ===
|
||||
|
||||
# Path to the pre-bundled gem directory inside the war file. Default
|
||||
# is 'WEB-INF/gems'. Specify path if gems are already bundled
|
||||
# before running Warbler. This also sets 'gem.path' inside web.xml.
|
||||
# config.gem_path = "WEB-INF/vendor/bundler_gems"
|
||||
|
||||
# Files for WEB-INF directory (next to web.xml). This contains
|
||||
# web.xml by default. If there is an .erb-File it will be processed
|
||||
# with webxml-config. You may want to exclude this file via
|
||||
# config.excludes.
|
||||
# config.webinf_files += FileList["jboss-web.xml"]
|
||||
|
||||
# Files to be included in the root of the webapp. Note that files in public
|
||||
# will have the leading 'public/' part of the path stripped during staging.
|
||||
# config.public_html = FileList["public/**/*", "doc/**/*"]
|
||||
|
||||
# Pathmaps for controlling how public HTML files are copied into the .war
|
||||
# config.pathmaps.public_html = ["%{public/,}p"]
|
||||
|
||||
# Embedded webserver to use with the 'executable' feature. Currently supported
|
||||
# webservers are:
|
||||
# * <tt>winstone</tt> (default) - Winstone 0.9.10 from sourceforge
|
||||
# * <tt>jenkins-ci.winstone</tt> - Improved Winstone from Jenkins CI
|
||||
# * <tt>jetty</tt> - Embedded Jetty from Eclipse
|
||||
# config.webserver = 'jetty'
|
||||
|
||||
# Value of RAILS_ENV for the webapp -- default as shown below
|
||||
# config.webxml.rails.env = ENV['RAILS_ENV'] || 'production'
|
||||
|
||||
# Application booter to use, one of :rack, :rails, or :merb (autodetected by default)
|
||||
# config.webxml.booter = :rails
|
||||
|
||||
# Set JRuby to run in 1.9 mode.
|
||||
# config.webxml.jruby.compat.version = "1.9"
|
||||
|
||||
# When using the :rack booter, "Rackup" script to use.
|
||||
# - For 'rackup.path', the value points to the location of the rackup
|
||||
# script in the web archive file. You need to make sure this file
|
||||
# gets included in the war, possibly by adding it to config.includes
|
||||
# or config.webinf_files above.
|
||||
# - For 'rackup', the rackup script you provide as an inline string
|
||||
# is simply embedded in web.xml.
|
||||
# The script is evaluated in a Rack::Builder to load the application.
|
||||
# Examples:
|
||||
# config.webxml.rackup.path = 'WEB-INF/hello.ru'
|
||||
# config.webxml.rackup = %{require './lib/demo'; run Rack::Adapter::Camping.new(Demo)}
|
||||
# config.webxml.rackup = require 'cgi' && CGI::escapeHTML(File.read("config.ru"))
|
||||
|
||||
# Control the pool of Rails runtimes. Leaving unspecified means
|
||||
# the pool will grow as needed to service requests. It is recommended
|
||||
# that you fix these values when running a production server!
|
||||
# If you're using threadsafe! mode, you probably don't want to set these values,
|
||||
# since 1 runtime(default for threadsafe mode) will be enough.
|
||||
# config.webxml.jruby.min.runtimes = 2
|
||||
# config.webxml.jruby.max.runtimes = 4
|
||||
|
||||
# JNDI data source name
|
||||
# config.webxml.jndi = 'jdbc/rails'
|
||||
end
|
12
server/config/web.ru
Normal file
12
server/config/web.ru
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Add the libs directory to the load path
|
||||
ROOT = File.expand_path("#{File.dirname(__FILE__)}/../")
|
||||
$LOAD_PATH.unshift(ROOT)
|
||||
|
||||
require "rubygems"
|
||||
require "bundler/setup"
|
||||
|
||||
# Require the application
|
||||
require "#{ROOT}/lib/app"
|
||||
|
||||
# Run the application
|
||||
run Kibana::App
|
27
server/lib/app.rb
Normal file
27
server/lib/app.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Add the root of the project to the $LOAD_PATH, For some reason it seems
|
||||
# to be getting lost when we use warble to make the jar. This fixes it :D
|
||||
$LOAD_PATH.unshift(ROOT)
|
||||
|
||||
require "rack/reverse_proxy"
|
||||
require "routes/home"
|
||||
require "routes/api"
|
||||
|
||||
module Kibana
|
||||
class App < Sinatra::Base
|
||||
|
||||
configure do
|
||||
set :root, ROOT
|
||||
set :public_folder, "#{ROOT}/public"
|
||||
set :httponly, true
|
||||
end
|
||||
|
||||
# Rack middleware goes here
|
||||
use Rack::ReverseProxy do
|
||||
reverse_proxy /^\/elasticsearch(.*)$/, 'http://localhost:9200$1'
|
||||
end
|
||||
|
||||
# Routes go here
|
||||
use Routes::Home
|
||||
use Routes::Api
|
||||
end
|
||||
end
|
7
server/lib/helpers.rb
Normal file
7
server/lib/helpers.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module Kibana
|
||||
module Helpers
|
||||
def doSomething()
|
||||
"Do it!"
|
||||
end
|
||||
end
|
||||
end
|
22
server/public/index.html
Normal file
22
server/public/index.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome to the Future Home of Kibana</title>
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center" style="padding-top: 40px;">
|
||||
<h1>Welcome to the Future Home of Kibana</h1>
|
||||
<p>This is the server component of Kibana. It's just a quick prototype of the things we need.</p>
|
||||
<p><a href="/test.html">Static Server</a></p>
|
||||
<p><a href="/api/foo">Server Side APIs</a></p>
|
||||
<p><a href="/elasticsearch">Elasticsearch Proxy</a></p>
|
||||
<h3>Much more coming soon...</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
9
server/public/test.html
Normal file
9
server/public/test.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>This is a test file</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>This is a test</h1>
|
||||
<p>This should work outside of anything else.</p>
|
||||
</body>
|
||||
</html>
|
16
server/routes/api.rb
Normal file
16
server/routes/api.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
require "routes/base"
|
||||
require "lib/helpers"
|
||||
|
||||
module Kibana
|
||||
module Routes
|
||||
class Api < Base
|
||||
|
||||
helpers Kibana::Helpers
|
||||
|
||||
get "/api/foo" do
|
||||
json :foo => doSomething()
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
15
server/routes/base.rb
Normal file
15
server/routes/base.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
require "sinatra/base"
|
||||
require "sinatra/json"
|
||||
|
||||
module Kibana
|
||||
module Routes
|
||||
class Base < Sinatra::Base
|
||||
helpers Sinatra::JSON
|
||||
|
||||
configure do
|
||||
# Confirgure stuffs here
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
13
server/routes/home.rb
Normal file
13
server/routes/home.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
require "routes/base"
|
||||
|
||||
module Kibana
|
||||
module Routes
|
||||
class Home < Base
|
||||
|
||||
get "/" do
|
||||
File.read(File.join(ROOT, 'public', 'index.html'))
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -46,7 +46,7 @@
|
|||
<div class="spinner">
|
||||
</div>
|
||||
</li>
|
||||
<li ng-if="setupComplete" ng-show="opts.timefilter.enabled()" class="navbar-timepicker-display">
|
||||
<li ng-if="setupComplete" ng-show="opts.timefilter.enabled" class="navbar-timepicker-display">
|
||||
<a ng-click="toggleTimepicker()">
|
||||
<pretty-duration from="opts.timefilter.time.from" to="opts.timefilter.time.to"></pretty-duration>
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
define(function (require) {
|
||||
var app = require('modules').get('app/dashboard');
|
||||
var _ = require('lodash');
|
||||
require('modules')
|
||||
.get('app/dashboard')
|
||||
.directive('dashboardPanel', function (savedVisualizations, Notifier) {
|
||||
var _ = require('lodash');
|
||||
|
||||
require('apps/visualize/directives/visualize');
|
||||
|
||||
app.directive('dashboardPanel', function (savedVisualizations, Notifier) {
|
||||
var notify = new Notifier();
|
||||
|
||||
require('components/visualize/visualize');
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: require('text!apps/dashboard/partials/panel.html'),
|
||||
|
@ -20,14 +22,12 @@ define(function (require) {
|
|||
if (!$scope.panel.visId) return;
|
||||
|
||||
savedVisualizations.get($scope.panel.visId)
|
||||
.then(function (vis) {
|
||||
$scope.vis = vis;
|
||||
.then(function (savedVis) {
|
||||
$scope.savedVis = savedVis;
|
||||
// .destroy() called by the visualize directive
|
||||
})
|
||||
.catch(function (e) {
|
||||
$scope.vis = {
|
||||
error: e
|
||||
};
|
||||
$scope.error = e.message;
|
||||
console.log(e);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -73,7 +73,7 @@ define(function (require) {
|
|||
$scope.openAdd = _.partial($scope.configTemplate.toggle, 'pickVis');
|
||||
$scope.refresh = _.bindKey(courier, 'fetch');
|
||||
|
||||
timefilter.enabled(true);
|
||||
timefilter.enabled = true;
|
||||
$scope.timefilter = timefilter;
|
||||
$scope.$watchCollection('globalState.time', $scope.refresh);
|
||||
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="panel-title">{{vis.title}}</span>
|
||||
<span class="panel-title">{{savedVis.title}}</span>
|
||||
<div class="btn-group">
|
||||
<a ng-show="!appEmbedded" ng-href="#visualize/edit/{{vis.title | uriescape}}"><i class="fa fa-pencil"></i></a>
|
||||
<a ng-show="!appEmbedded" ng-href="#visualize/edit/{{savedVis.title | uriescape}}"><i class="fa fa-pencil"></i></a>
|
||||
<a ng-show="!appEmbedded" ng-click="remove()"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<visualize vis="vis"></visualize>
|
||||
|
||||
<div ng-if="error" class="load-error">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<span ng-bind="error"></span>
|
||||
</div>
|
||||
<visualize ng-if="savedVis" vis="savedVis.vis" search-source="savedVis.searchSource"></visualize>
|
||||
</div>
|
|
@ -166,6 +166,36 @@ dashboard-grid {
|
|||
.gridster dashboard-panel .panel .panel-heading a:hover {
|
||||
color: #298fa3;
|
||||
}
|
||||
.gridster dashboard-panel .panel .load-error {
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
-webkit-box-pack: center;
|
||||
-moz-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
.gridster dashboard-panel .panel .load-error .fa-exclamation-triangle {
|
||||
font-size: 2em;
|
||||
color: #e74c3c;
|
||||
}
|
||||
.gridster dashboard-panel .panel visualize {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@import (reference) "../../../styles/_bootstrap.less";
|
||||
@import (reference) "../../../styles/theme/_theme.less";
|
||||
@import (reference) "../../../styles/_mixins.less";
|
||||
@import (reference) "lesshat.less";
|
||||
|
||||
@dashboard-background: @gray-light;
|
||||
|
@ -71,9 +72,7 @@ dashboard-grid {
|
|||
|
||||
.panel-title {
|
||||
font-size: inherit;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
.ellipsis();
|
||||
.flex(1 1 0);
|
||||
}
|
||||
|
||||
|
@ -88,6 +87,20 @@ dashboard-grid {
|
|||
}
|
||||
}
|
||||
|
||||
.load-error {
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
.display(flex);
|
||||
.flex(1 0 auto);
|
||||
.justify-content(center);
|
||||
.flex-direction(column);
|
||||
|
||||
.fa-exclamation-triangle {
|
||||
font-size: 2em;
|
||||
color: @btn-danger-bg;
|
||||
}
|
||||
}
|
||||
|
||||
visualize {
|
||||
.flex(1, 1, 100%);
|
||||
height: auto;
|
||||
|
|
|
@ -177,17 +177,20 @@ define(function (require) {
|
|||
|
||||
if (!resp.aggregations) return;
|
||||
|
||||
var aggKey = _.find(Object.keys(resp.aggregations), function (key) {
|
||||
return key.substr(0, 5) === '_agg_';
|
||||
});
|
||||
|
||||
// start merging aggregations
|
||||
if (!merged.aggregations) {
|
||||
merged.aggregations = {
|
||||
_agg_0: {
|
||||
buckets: []
|
||||
}
|
||||
merged.aggregations = {};
|
||||
merged.aggregations[aggKey] = {
|
||||
buckets: []
|
||||
};
|
||||
merged._bucketIndex = {};
|
||||
}
|
||||
|
||||
resp.aggregations._agg_0.buckets.forEach(function (bucket) {
|
||||
resp.aggregations[aggKey].buckets.forEach(function (bucket) {
|
||||
var mbucket = merged._bucketIndex[bucket.key];
|
||||
if (mbucket) {
|
||||
mbucket.doc_count += bucket.doc_count;
|
||||
|
@ -195,7 +198,7 @@ define(function (require) {
|
|||
}
|
||||
|
||||
mbucket = merged._bucketIndex[bucket.key] = bucket;
|
||||
merged.aggregations._agg_0.buckets.push(mbucket);
|
||||
merged.aggregations[aggKey].buckets.push(mbucket);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -21,8 +21,6 @@ define(function (require) {
|
|||
|
||||
require('apps/discover/directives/table');
|
||||
|
||||
require('apps/visualize/saved_visualizations/_adhoc_vis');
|
||||
|
||||
var app = require('modules').get('apps/discover', [
|
||||
'kibana/notify',
|
||||
'kibana/courier',
|
||||
|
@ -49,8 +47,9 @@ define(function (require) {
|
|||
|
||||
|
||||
app.controller('discover', function ($scope, config, courier, $route, $window, savedSearches, savedVisualizations,
|
||||
Notifier, $location, globalState, appStateFactory, timefilter, AdhocVis, Promise, Private) {
|
||||
Notifier, $location, globalState, appStateFactory, timefilter, Promise, Private) {
|
||||
|
||||
var Vis = Private(require('components/vis/vis'));
|
||||
var segmentedFetch = $scope.segmentedFetch = Private(require('apps/discover/_segmented_fetch'));
|
||||
var HitSortFn = Private(require('apps/discover/_hit_sort_fn'));
|
||||
var diffTimePickerValues = Private(require('utils/diff_time_picker_vals'));
|
||||
|
@ -127,9 +126,6 @@ define(function (require) {
|
|||
indexPatternList: indexPatternList,
|
||||
};
|
||||
|
||||
// So we can watch it.
|
||||
$scope.time = timefilter.time;
|
||||
|
||||
// stores the complete list of fields
|
||||
$scope.fields = null;
|
||||
|
||||
|
@ -153,9 +149,8 @@ define(function (require) {
|
|||
if (_.difference(changed, ignoreStateChanges).length) $scope.fetch();
|
||||
});
|
||||
|
||||
// TODO: Switch this to watching time.string when we implement it
|
||||
$scope.$watchCollection('globalState.time', function (newTime, oldTime) {
|
||||
if (diffTimePickerValues(newTime, oldTime)) $scope.fetch();
|
||||
timefilter.on('update', function () {
|
||||
$scope.fetch();
|
||||
});
|
||||
|
||||
$scope.$watch('state.sort', function (sort) {
|
||||
|
@ -173,7 +168,7 @@ define(function (require) {
|
|||
});
|
||||
|
||||
$scope.$watch('opts.timefield', function (timefield) {
|
||||
timefilter.enabled(!!timefield);
|
||||
timefilter.enabled = !!timefield;
|
||||
});
|
||||
|
||||
// options are 'loading', 'ready', 'none', undefined
|
||||
|
@ -591,22 +586,23 @@ define(function (require) {
|
|||
// we shouldn't have a vis, delete it
|
||||
if (!$scope.opts.timefield && $scope.vis) {
|
||||
$scope.vis.destroy();
|
||||
$scope.searchSource.set('aggs', undefined);
|
||||
delete $scope.vis;
|
||||
}
|
||||
// we shouldn't have one, or already do, return whatever we already have
|
||||
if (!$scope.opts.timefield || $scope.vis) return Promise.resolve($scope.vis);
|
||||
|
||||
var vis = new AdhocVis({
|
||||
searchSource: $scope.searchSource,
|
||||
// TODO: a legit way to update the index pattern
|
||||
$scope.vis = new Vis($scope.searchSource.get('index'), {
|
||||
type: 'histogram',
|
||||
listeners: {
|
||||
onClick: function (e) {
|
||||
click: function (e) {
|
||||
console.log(e);
|
||||
timefilter.time.from = moment(e.point.x);
|
||||
timefilter.time.to = moment(e.point.x + e.data.ordered.interval);
|
||||
timefilter.time.mode = 'absolute';
|
||||
},
|
||||
onBrush: function (e) {
|
||||
brush: function (e) {
|
||||
var from = moment(e.range[0]);
|
||||
var to = moment(e.range[1]);
|
||||
|
||||
|
@ -617,40 +613,36 @@ define(function (require) {
|
|||
timefilter.time.mode = 'absolute';
|
||||
}
|
||||
},
|
||||
config: {
|
||||
metric: {
|
||||
configs: [{
|
||||
agg: 'count',
|
||||
}]
|
||||
aggs: [
|
||||
{
|
||||
type: 'count',
|
||||
schema: 'metric'
|
||||
},
|
||||
segment: {
|
||||
configs: [{
|
||||
agg: 'date_histogram',
|
||||
{
|
||||
type: 'date_histogram',
|
||||
schema: 'segment',
|
||||
params: {
|
||||
field: $scope.opts.timefield,
|
||||
interval: $state.interval,
|
||||
min_doc_count: 0,
|
||||
}]
|
||||
},
|
||||
group: { configs: [] },
|
||||
split: { configs: [] },
|
||||
}
|
||||
interval: 'auto',
|
||||
min_doc_count: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
$scope.searchSource.aggs(function () {
|
||||
return $scope.vis.aggs.toDSL();
|
||||
});
|
||||
|
||||
// stash this promise so that other calls to setupVisualization will have to wait
|
||||
loadingVis = vis.init()
|
||||
.then(function () {
|
||||
// expose the vis so that the visualize directive can get started
|
||||
$scope.vis = vis;
|
||||
|
||||
// wait for visualize directive to emit that it's ready before resolving
|
||||
return new Promise(function (resolve) {
|
||||
$scope.$on('ready:vis', resolve);
|
||||
loadingVis = new Promise(function (resolve) {
|
||||
$scope.$on('ready:vis', function () {
|
||||
resolve($scope.vis);
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
.finally(function () {
|
||||
// clear the loading flag
|
||||
loadingVis = null;
|
||||
return vis;
|
||||
});
|
||||
|
||||
return loadingVis;
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
<div class="spinner large"> </div>
|
||||
</div>
|
||||
|
||||
<visualize vis="vis" es-resp="mergedEsResp"></visualize>
|
||||
<visualize vis="vis" es-resp="mergedEsResp" search-source="searchSource"></visualize>
|
||||
</div>
|
||||
|
||||
<div class="discover-table"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
<li bindonce class="sidebar-item" bo-attr bo-attr-field="field.name">
|
||||
<div ng-click="toggleDetails(field)" class="sidebar-item-title">
|
||||
<field-name field="field" title="{{field.name}}"></field-name>
|
||||
<field-name field="field"></field-name>
|
||||
<button
|
||||
ng-click="toggleDisplay(field)"
|
||||
bo-class="field.display ? 'btn-danger' : 'btn-primary'"
|
||||
|
|
|
@ -19,7 +19,7 @@ define(function (require, module, exports) {
|
|||
sectionName: '@section'
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
timefilter.enabled(false);
|
||||
timefilter.enabled = false;
|
||||
$scope.sections = require('apps/settings/sections/index');
|
||||
$scope.section = _.find($scope.sections, { name: $scope.sectionName });
|
||||
|
||||
|
|
|
@ -1,259 +0,0 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var angular = require('angular');
|
||||
var ConfigTemplate = require('utils/config_template');
|
||||
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
|
||||
var qs = require('utils/query_string');
|
||||
|
||||
require('apps/visualize/saved_visualizations/saved_visualizations');
|
||||
require('components/notify/notify');
|
||||
require('filters/uriescape');
|
||||
|
||||
var app = require('modules').get('apps/visualize', [
|
||||
'kibana/notify',
|
||||
'kibana/courier'
|
||||
]);
|
||||
|
||||
var visConfigCategories = require('apps/visualize/saved_visualizations/_config_categories');
|
||||
|
||||
require('routes')
|
||||
.when('/visualize/create', {
|
||||
template: require('text!apps/visualize/editor.html'),
|
||||
resolve: {
|
||||
vis: function (savedVisualizations, courier, $route) {
|
||||
if (!$route.current.params.indexPattern && !$route.current.params.savedSearchId) {
|
||||
throw new Error('You must provide either an indexPattern or a savedSearchId');
|
||||
}
|
||||
|
||||
return savedVisualizations.get($route.current.params)
|
||||
.catch(courier.redirectWhenMissing({
|
||||
//'index-pattern': '/visualize',
|
||||
'*': '/visualize'
|
||||
}));
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/visualize/edit/:id', {
|
||||
template: require('text!apps/visualize/editor.html'),
|
||||
resolve: {
|
||||
vis: function (savedVisualizations, courier, $route) {
|
||||
return savedVisualizations.get($route.current.params.id)
|
||||
.catch(courier.redirectWhenMissing({
|
||||
'index-pattern': '/settings',
|
||||
'*': '/visualize'
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.controller('VisualizeEditor', function ($scope, $route, $timeout, $window, Notifier, $location,
|
||||
globalState, appStateFactory, timefilter, Private) {
|
||||
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
|
||||
|
||||
var notify = new Notifier({
|
||||
location: 'Visualization Editor'
|
||||
});
|
||||
|
||||
// get the vis loaded in from the routes
|
||||
var vis = $route.current.locals.vis;
|
||||
// vis.destroy called by visualize directive
|
||||
|
||||
var indexPattern = vis.searchSource.get('index');
|
||||
|
||||
$scope.fields = _.sortBy(indexPattern.fields, 'name');
|
||||
$scope.fields.byName = indexPattern.fieldsByName;
|
||||
|
||||
var $state = $scope.state = appStateFactory.create(vis.getState());
|
||||
|
||||
if ($state.query) {
|
||||
vis.searchSource.set('query', $state.query);
|
||||
}
|
||||
|
||||
$scope.vis = vis;
|
||||
|
||||
$scope.aggs = aggs;
|
||||
$scope.visConfigCategories = visConfigCategories;
|
||||
|
||||
var visConfigProperties = Object.keys(visConfigCategories.byName);
|
||||
|
||||
var init = function () {
|
||||
$scope.$on('ready:vis', function () {
|
||||
// once the visualization is ready, boot up
|
||||
vis.setState($state);
|
||||
watchForConfigChanges();
|
||||
$scope.$emit('application.load');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* YOU PROBABLY WANT write|readStateAndFetch
|
||||
*/
|
||||
var justFetch = function () {
|
||||
// we use state to track query, must write before we fetch
|
||||
if ($state.query) {
|
||||
vis.searchSource.set('query', $state.query);
|
||||
} else {
|
||||
vis.searchSource.set('query', null);
|
||||
}
|
||||
vis.searchSource.fetch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Write the latest changes made on the visualization to the $state. This
|
||||
* will cause a fetch if there were changes
|
||||
*
|
||||
* @return {Array} - a list of the keys from state that were updated.
|
||||
*/
|
||||
var writeStateAndFetch = function () {
|
||||
_.assign($state, vis.getState());
|
||||
watchForConfigChanges();
|
||||
$state.save();
|
||||
justFetch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Pull the state into the vis, and then fetch the searchSource
|
||||
* @return {undefined}
|
||||
*/
|
||||
var readStateAndFetch = function () {
|
||||
// update and commit the state, which will update the vis dataSource if there were new changes
|
||||
vis.setState($state);
|
||||
watchForConfigChanges();
|
||||
justFetch();
|
||||
};
|
||||
|
||||
var watchForConfigChanges = (function () {
|
||||
var _unwatchers = [];
|
||||
var _clearWatchers = function () {
|
||||
_unwatchers.length && _unwatchers.splice(0).forEach(function (unwatcher) { unwatcher(); });
|
||||
};
|
||||
|
||||
return function () {
|
||||
$scope.vis.dirty = false;
|
||||
_clearWatchers();
|
||||
// watch config properties for deep changes
|
||||
visConfigProperties.forEach(function (prop) {
|
||||
_unwatchers.push($scope.$watch('vis.' + prop + '.configs', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return; // stupid initRun
|
||||
$scope.vis.dirty = true;
|
||||
_clearWatchers();
|
||||
}, true));
|
||||
});
|
||||
};
|
||||
}());
|
||||
|
||||
/**
|
||||
* When something else updates the state, let us know
|
||||
*/
|
||||
$state.onUpdate(readStateAndFetch);
|
||||
|
||||
/**
|
||||
* Click handler for the "refresh" button
|
||||
*/
|
||||
$scope.doVisualize = writeStateAndFetch;
|
||||
|
||||
/**
|
||||
* Click handler for the "new doc" button
|
||||
*/
|
||||
$scope.startOver = function () {
|
||||
$location.url('/visualize');
|
||||
};
|
||||
|
||||
/**
|
||||
* Do that actual save, click handler for the "save" button within the save config panel
|
||||
*/
|
||||
$scope.doSave = function () {
|
||||
writeStateAndFetch();
|
||||
|
||||
// use the title for the id
|
||||
vis.id = vis.title;
|
||||
|
||||
// serialize the current state
|
||||
vis.stateJSON = JSON.stringify(vis.getState());
|
||||
|
||||
vis.save()
|
||||
.then(function () {
|
||||
if (vis.id !== $route.current.params.id) {
|
||||
$location.url(globalState.writeToUrl('/visualize/edit/' + encodeURIComponent(vis.id)));
|
||||
}
|
||||
configTemplate.close('save');
|
||||
}, notify.fatal);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Enable the timefilter, and tell Angular to
|
||||
*/
|
||||
timefilter.enabled(true);
|
||||
$scope.timefilter = timefilter;
|
||||
// TODO: Switch this to watching time.string when we implement it
|
||||
$scope.$watchCollection('timefilter.time', function (newTime, oldTime) {
|
||||
// don't fetch unless there was a previous value and the values are not loosly equal
|
||||
if (!_.isUndefined(oldTime) && !angular.equals(newTime, oldTime)) $scope.doVisualize();
|
||||
});
|
||||
|
||||
// config panel templates
|
||||
var configTemplate = $scope.configTemplate = new ConfigTemplate({
|
||||
save: require('text!apps/visualize/partials/save.html'),
|
||||
load: require('text!apps/visualize/partials/load.html'),
|
||||
share: require('text!apps/visualize/partials/share.html'),
|
||||
});
|
||||
|
||||
$scope.toggleShare = _.bindKey(configTemplate, 'toggle', 'share');
|
||||
$scope.shareData = function () {
|
||||
return {
|
||||
link: $location.absUrl(),
|
||||
// This sucks, but seems like the cleanest way. Uhg.
|
||||
embed: $location.absUrl().replace('?', '?embed&')
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Click handler for the "save" button.
|
||||
*/
|
||||
$scope.toggleSave = _.bindKey(configTemplate, 'toggle', 'save');
|
||||
|
||||
/**
|
||||
* Toggle the load config panel
|
||||
*/
|
||||
$scope.toggleLoad = _.bindKey(configTemplate, 'toggle', 'load');
|
||||
|
||||
// objects to make available within the config panel's scope
|
||||
$scope.conf = _.pick($scope, 'doSave', 'vis', 'shareData');
|
||||
|
||||
$scope.unlink = function () {
|
||||
// display unlinking for 2 seconds, unless it is double clicked
|
||||
$scope.unlinking = $timeout($scope.doneUnlinking, 2000);
|
||||
|
||||
delete vis.savedSearchId;
|
||||
|
||||
var q = vis.searchSource.get('query');
|
||||
$state.query = q;
|
||||
|
||||
var parent = vis.searchSource.parent();
|
||||
// we will copy over all state minus the "aggs"
|
||||
_(parent.toJSON()).omit('aggs').forOwn(function (val, key) {
|
||||
vis.searchSource.set(key, val);
|
||||
});
|
||||
|
||||
vis.searchSource.inherits(parent.parent());
|
||||
};
|
||||
$scope.doneUnlinking = function () {
|
||||
$scope.unlinking = clearTimeout($scope.unlinking);
|
||||
$scope.linked = false;
|
||||
};
|
||||
|
||||
$scope.linked = !!vis.savedSearchId;
|
||||
if ($scope.linked) {
|
||||
// possibly left over state from unsaved unlinking
|
||||
delete $state.query;
|
||||
} else {
|
||||
var q = $state.query || vis.searchSource.get('query');
|
||||
$state.query = q;
|
||||
}
|
||||
|
||||
// init
|
||||
init();
|
||||
});
|
||||
|
||||
});
|
|
@ -1,79 +0,0 @@
|
|||
define(function (require) {
|
||||
var module = require('modules').get('apps/visualize');
|
||||
var $ = require('jquery');
|
||||
var _ = require('lodash');
|
||||
|
||||
module.directive('visCanvas', function ($http) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
vis: '='
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
var $window = $(window);
|
||||
var $body = $(document.body);
|
||||
|
||||
var vals = {
|
||||
windowHeight: function () {
|
||||
return $window.height();
|
||||
},
|
||||
offsetTop: function () {
|
||||
return $el.offset().top;
|
||||
}
|
||||
};
|
||||
|
||||
var cur = {};
|
||||
|
||||
var needRender = function () {
|
||||
var need = false;
|
||||
_.forOwn(vals, function (get, name) {
|
||||
var val = get();
|
||||
if (cur[name] !== val) {
|
||||
need = true;
|
||||
cur[name] = val;
|
||||
}
|
||||
});
|
||||
return need;
|
||||
};
|
||||
|
||||
var render = function () {
|
||||
var parentPadding = _.reduce($el.parents().toArray(), function (padding, parent) {
|
||||
var $parent = $(parent);
|
||||
return padding + (parseInt($parent.css('paddingBottom'), 10) || 0) - (parseInt($parent.css('marginBottom'), 10) || 0);
|
||||
}, 0);
|
||||
|
||||
$el.css('height', cur.windowHeight - cur.offsetTop - parentPadding);
|
||||
};
|
||||
|
||||
var poll = function () {
|
||||
if (poll.id) clearTimeout(poll.id);
|
||||
poll.count = 0;
|
||||
|
||||
(function check() {
|
||||
var need = needRender();
|
||||
if (need) {
|
||||
poll.count = 0;
|
||||
render();
|
||||
} else {
|
||||
poll.count ++;
|
||||
}
|
||||
|
||||
if (poll.count < 5) poll.id = setTimeout(check, 100);
|
||||
}());
|
||||
};
|
||||
|
||||
$window.on('resize', poll);
|
||||
$body.on('mousedown mouseup', poll);
|
||||
|
||||
$scope.pendingHttpRequests = $http.pendingRequests;
|
||||
$scope.$watch('pendingHttpRequests.length', poll);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
$window.off('resize', poll);
|
||||
$body.off('mousedown mouseup', poll);
|
||||
clearTimeout(poll.id);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
define(function (require) {
|
||||
var html = require('text!apps/visualize/partials/config_category.html');
|
||||
|
||||
require('apps/visualize/directives/config_editor');
|
||||
|
||||
require('modules')
|
||||
.get('apps/visualize')
|
||||
.directive('visConfigCategory', function (Private) {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
category: '=',
|
||||
vis: '=',
|
||||
fields: '='
|
||||
},
|
||||
template: html,
|
||||
link: function ($scope, $el) {
|
||||
$scope.moveHandler = function (config, delta) {
|
||||
var configs = $scope.category.configs;
|
||||
var i = configs.indexOf(config);
|
||||
if (delta === false) {
|
||||
// means remove
|
||||
configs.splice(i, 1);
|
||||
} else {
|
||||
// move to a new position (iTarget)
|
||||
var iTarget = Math.max(0, Math.min(configs.length - 1, i + delta));
|
||||
if (i !== iTarget) {
|
||||
configs.splice(iTarget, 0, configs.splice(i, 1).pop());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -1,195 +0,0 @@
|
|||
define(function (require) {
|
||||
var app = require('modules').get('apps/visualize');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
|
||||
require('filters/field_type');
|
||||
|
||||
var visConfigCategories = require('apps/visualize/saved_visualizations/_config_categories');
|
||||
|
||||
var headerHtml = require('text!apps/visualize/partials/editor/header.html');
|
||||
|
||||
var controlHtml = {
|
||||
ranges: require('text!apps/visualize/partials/controls/ranges.html'),
|
||||
ip_range: require('text!apps/visualize/partials/controls/ip_range.html'),
|
||||
filters: require('text!apps/visualize/partials/controls/filters.html'),
|
||||
orderAndSize: require('text!apps/visualize/partials/controls/order_and_size.html'),
|
||||
minDocCount: require('text!apps/visualize/partials/controls/min_doc_count.html'),
|
||||
extendedBounds: require('text!apps/visualize/partials/controls/extended_bounds.html'),
|
||||
interval: require('text!apps/visualize/partials/controls/interval.html'),
|
||||
globalLocal: require('text!apps/visualize/partials/controls/global_local.html')
|
||||
};
|
||||
|
||||
app.directive('visConfigEditor', function ($compile, Private) {
|
||||
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
|
||||
|
||||
var categoryOptions = {
|
||||
metric: {
|
||||
template: require('text!apps/visualize/partials/editor/metric.html')
|
||||
},
|
||||
segment: {
|
||||
template: require('text!apps/visualize/partials/editor/dimension.html'),
|
||||
setup: setupDimension
|
||||
},
|
||||
group: {
|
||||
template: require('text!apps/visualize/partials/editor/dimension.html'),
|
||||
setup: setupDimension
|
||||
},
|
||||
split: {
|
||||
template: require('text!apps/visualize/partials/editor/dimension.html'),
|
||||
setup: setupDimension
|
||||
}
|
||||
};
|
||||
|
||||
// generalized setup for group and segment
|
||||
function setupDimension($scope, $el) {
|
||||
var $controls = $el.find('.agg-param-controls');
|
||||
|
||||
function getAvailableAggsForField() {
|
||||
if (!$scope.config.field || !$scope.fields) return;
|
||||
|
||||
var field = $scope.fields.byName[$scope.config.field];
|
||||
|
||||
// clear the previous choices
|
||||
$scope.availableAggs = void 0;
|
||||
|
||||
|
||||
var options = [
|
||||
aggs.bucketAggsByName.terms,
|
||||
aggs.bucketAggsByName.histogram,
|
||||
aggs.bucketAggsByName.range,
|
||||
aggs.bucketAggsByName.ip_range,
|
||||
aggs.bucketAggsByName.date_histogram,
|
||||
aggs.bucketAggsByName.filters,
|
||||
// 'range'
|
||||
];
|
||||
|
||||
// get the new choices
|
||||
//var options = aggs.byFieldType[field.type];
|
||||
|
||||
if (!options || options.length === 0) {
|
||||
// init or invalid field type
|
||||
$scope.config.agg = void 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.length === 1) {
|
||||
// only once choice, make it for the user
|
||||
$scope.config.agg = options[0].name;
|
||||
return;
|
||||
}
|
||||
|
||||
// set the new choices
|
||||
$scope.availableAggs = options;
|
||||
|
||||
// update the agg only if it is not currently a valid option
|
||||
if (!$scope.config.agg || !_.find(options, { name: $scope.config.agg })) {
|
||||
$scope.config.agg = options[0].name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// since this depends on the field and field list, watch both
|
||||
// this doesn't trigger when we switch the metric agg field?
|
||||
$scope.$watch('config.field', function (field) {
|
||||
getAvailableAggsForField(field);
|
||||
if ($scope.vis && $scope.vis.searchSource) {
|
||||
$scope.vis.searchSource.get('index').popularizeField(field, 1);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('fields', getAvailableAggsForField);
|
||||
|
||||
$scope.$watch('config.agg', function (aggName) {
|
||||
var agg = aggs.byName[aggName];
|
||||
var controlsHtml = '';
|
||||
|
||||
if (agg) {
|
||||
var params = $scope.aggParams = agg.params;
|
||||
|
||||
_.forOwn(params, function (param, name) {
|
||||
// if the param doesn't have options, or a default value, skip it
|
||||
if (!param.options) return;
|
||||
// if there isn't currently a value, or the current value is not one of the options, set it to the default
|
||||
if (!$scope.config[name] || !_.find(param.options, { val: $scope.config[name] })) {
|
||||
$scope.config[name] = param.default;
|
||||
}
|
||||
});
|
||||
|
||||
if (params.order && params.size && !params.order.hide) {
|
||||
controlsHtml += ' ' + controlHtml.orderAndSize;
|
||||
}
|
||||
|
||||
if (params.interval && !params.interval.hide) {
|
||||
controlsHtml += ' ' + controlHtml.interval;
|
||||
if (!controlsHtml.match(/aggParams\.interval\.options/)) ; //debugger;
|
||||
}
|
||||
|
||||
if (aggName === 'range' && params.ranges) {
|
||||
controlsHtml += ' ' + controlHtml.ranges;
|
||||
}
|
||||
|
||||
if (aggName === 'ip_range' && params.ranges) {
|
||||
controlsHtml += ' ' + controlHtml.ip_range;
|
||||
}
|
||||
|
||||
if (params.filters) {
|
||||
controlsHtml += ' ' + controlHtml.filters;
|
||||
}
|
||||
|
||||
if (params.min_doc_count && !params.min_doc_count.hide) {
|
||||
controlsHtml += ' ' + controlHtml.minDocCount;
|
||||
}
|
||||
|
||||
if (params.extended_bounds && !params.extended_bounds.hide) {
|
||||
controlsHtml += ' ' + controlHtml.extendedBounds;
|
||||
}
|
||||
|
||||
if ($scope.category.name === 'group') {
|
||||
controlsHtml += ' ' + controlHtml.globalLocal;
|
||||
}
|
||||
}
|
||||
|
||||
$controls.html($compile(controlsHtml)($scope));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
config: '=',
|
||||
category: '=',
|
||||
fields: '=',
|
||||
vis: '=',
|
||||
move: '='
|
||||
},
|
||||
link: function ($scope, $el, attr) {
|
||||
$scope.aggs = aggs;
|
||||
$scope.visConfigCategories = visConfigCategories;
|
||||
|
||||
$scope.$watch('category', function (category, prevCategory) {
|
||||
// clear out the previous state if necessary
|
||||
if (prevCategory && !category) {
|
||||
delete $scope[category.name];
|
||||
$el.html('');
|
||||
return;
|
||||
}
|
||||
// no work to be done yet
|
||||
if (!category) return;
|
||||
|
||||
var opts = categoryOptions[category.name];
|
||||
|
||||
// attach a copy of the template to the scope and render
|
||||
$el.html($compile(headerHtml + '\n' + opts.template)($scope));
|
||||
|
||||
_.defaults($scope.val, opts.defVal || {});
|
||||
if (opts.setup) opts.setup($scope, $el);
|
||||
|
||||
// rather than accessing vis.{{categoryName}} everywhere
|
||||
$scope[category.name] = $scope.vis[category.name];
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
define(function (require) {
|
||||
var module = require('modules').get('apps/visualize');
|
||||
module.directive('visSearchEditor', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
vis: '='
|
||||
},
|
||||
template: require('text!apps/visualize/partials/search_editor.html'),
|
||||
link: function ($scope, $el) {
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -1,155 +0,0 @@
|
|||
define(function (require) {
|
||||
var vislib = require('components/vislib/index');
|
||||
var $ = require('jquery');
|
||||
var _ = require('lodash');
|
||||
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
|
||||
|
||||
|
||||
require('css!apps/visualize/styles/visualization.css');
|
||||
require('apps/visualize/spy/spy');
|
||||
|
||||
var module = require('modules').get('kibana/directive');
|
||||
|
||||
module.directive('visualize', function (createNotifier, SavedVis, indexPatterns, visLib) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope : {
|
||||
vis: '=',
|
||||
esResp: '=?'
|
||||
},
|
||||
template: require('text!apps/visualize/partials/visualize.html'),
|
||||
link: function ($scope, $el, attr) {
|
||||
var chart; // set in "vis" watcher
|
||||
var notify = createNotifier();
|
||||
|
||||
var $visualize = $el.find('.visualize-chart');
|
||||
var $spy = $el.find('visualize-spy');
|
||||
|
||||
$scope.fields = {};
|
||||
$scope.spyMode = false;
|
||||
$scope.onlyShowSpy = false;
|
||||
|
||||
var applyClassNames = function () {
|
||||
// external
|
||||
$el.toggleClass('only-visualization', !$scope.spyMode);
|
||||
$el.toggleClass('visualization-and-spy', $scope.spyMode && !$scope.onlyShowSpy);
|
||||
$el.toggleClass('only-spy', Boolean($scope.onlyShowSpy));
|
||||
|
||||
$spy.toggleClass('only', Boolean($scope.onlyShowSpy));
|
||||
|
||||
// internal
|
||||
$visualize.toggleClass('spy-visible', Boolean($scope.spyMode));
|
||||
$visualize.toggleClass('spy-only', Boolean($scope.onlyShowSpy));
|
||||
};
|
||||
|
||||
var calcResponsiveStuff = function () {
|
||||
$scope.onlyShowSpy = $scope.spyMode && $el.height() < 550;
|
||||
};
|
||||
|
||||
var render = function () {
|
||||
applyClassNames();
|
||||
|
||||
if (chart && $scope.chartData && !$scope.onlyShowSpy) {
|
||||
notify.event('call chart render', function () {
|
||||
chart.render($scope.chartData);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// provide a setter to the visualize-spy directive
|
||||
$scope.$on('change:spyMode', function (event, newMode) {
|
||||
calcResponsiveStuff();
|
||||
render();
|
||||
});
|
||||
|
||||
$scope.$watch('vis', function (vis, prevVis) {
|
||||
if (prevVis && vis !== prevVis && prevVis.destroy) prevVis.destroy();
|
||||
if (chart) {
|
||||
chart.off('hover');
|
||||
chart.off('click');
|
||||
chart.destroy();
|
||||
}
|
||||
|
||||
if (!vis) return;
|
||||
|
||||
if (vis.error) {
|
||||
$el.html('<div class="visualize-error"><i class="fa fa-exclamation-triangle"></i><br>' + vis.error + '</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var typeDefinition = typeDefs.byName[vis.typeName];
|
||||
var params = {
|
||||
type: vis.typeName,
|
||||
};
|
||||
|
||||
$scope.fields = vis.searchSource.get('index').fieldsByName;
|
||||
|
||||
_.merge(params, vis.params);
|
||||
_.defaults(params, typeDefinition.params);
|
||||
|
||||
chart = new visLib.Vis($visualize[0], params);
|
||||
|
||||
// For each type of interaction, assign the the handler if the vis object has it
|
||||
// otherwise use the typeDef, otherwise, do nothing.
|
||||
_.each({hover: 'onHover', click: 'onClick', brush: 'onBrush'}, function (func, event) {
|
||||
var callback = vis[func] || typeDefinition[func];
|
||||
if (!!callback) chart.on(event, callback);
|
||||
});
|
||||
|
||||
|
||||
if (!attr.esResp) {
|
||||
// fetch the response ourselves if it's not provided
|
||||
vis.searchSource.onResults(function onResults(resp) {
|
||||
$scope.esResp = resp;
|
||||
}).catch(notify.fatal);
|
||||
}
|
||||
|
||||
|
||||
vis.searchSource.onError(notify.error).catch(notify.fatal);
|
||||
|
||||
$scope.$root.$broadcast('ready:vis');
|
||||
});
|
||||
|
||||
$scope.$watch('esResp', function (resp, prevResp) {
|
||||
if (!resp) return;
|
||||
|
||||
var vis = $scope.vis;
|
||||
var source = vis.searchSource;
|
||||
|
||||
$scope.chartData = vis.buildChartDataFromResponse($scope.vis.searchSource.get('index'), resp);
|
||||
});
|
||||
|
||||
$scope.$watch('chartData', render);
|
||||
|
||||
$scope.$on('resize', function () {
|
||||
var old;
|
||||
(function waitForAnim() {
|
||||
var cur = $el.width() + ':' + $el.height();
|
||||
if (cur !== old) {
|
||||
old = cur;
|
||||
// resize can sometimes be called before animations on the element are complete.
|
||||
// check each 50ms if the animations are complete and then render when they are
|
||||
return setTimeout(waitForAnim, 200);
|
||||
}
|
||||
|
||||
calcResponsiveStuff();
|
||||
applyClassNames();
|
||||
|
||||
// chart reference changes over time, don't bind to a specific chart object.
|
||||
if (chart) chart.resize();
|
||||
}());
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
// Vis with missing indexpattern will not have destroy
|
||||
if ($scope.vis && $scope.vis.destroy) $scope.vis.destroy();
|
||||
if (chart) {
|
||||
chart.off('hover');
|
||||
chart.off('click');
|
||||
chart.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -1,97 +0,0 @@
|
|||
<div ng-controller="VisualizeEditor" class="vis-editor">
|
||||
<div ng-if="!appEmbedded" class="app-container">
|
||||
<navbar>
|
||||
<span ng-if="!!vis.title" class="name" ng-bind="vis.title"></span>
|
||||
<div class="fill bitty-modal-container">
|
||||
<div ng-if="linked && !unlinking"
|
||||
ng-dblclick="unlink()"
|
||||
tooltip="Double click to unlink this visualization from the saved search"
|
||||
class="bitty-modal visualize-linked">
|
||||
<i class="fa fa-link"></i>
|
||||
|
||||
This visualization is linked to a saved search:
|
||||
<b>{{ vis.savedSearchId | json}}</b>
|
||||
<a href="#/discover/{{ vis.savedSearchId | uriescape }}">
|
||||
<i class="fa fa-pencil" tooltip="Click here to edit the linked saved search"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="linked && unlinking" ng-click="doneUnlinking()" class="bitty-modal">
|
||||
<i class="fa fa-chain-broken"></i> Unlinked!
|
||||
</div>
|
||||
|
||||
<form ng-submit="doVisualize()" class="inline-form" name="queryInput">
|
||||
<div class="typeahead" kbn-typeahead="visualize">
|
||||
<div class="input-group"
|
||||
ng-class="queryInput.$invalid ? 'has-error' : ''">
|
||||
<input query-input="vis.searchSource" input-focus
|
||||
kbn-typeahead-input
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="state.query">
|
||||
|
||||
<button class="btn btn-default" type="submit"
|
||||
ng-disabled="queryInput.$invalid">
|
||||
<span class="fa fa-search"></span>
|
||||
</button>
|
||||
</div>
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button ng-click="startOver()"><i class="fa fa-file-o"></i></button>
|
||||
<button ng-click="toggleSave()"><i class="fa fa-save"></i></button>
|
||||
<button ng-click="toggleLoad()"><i class="fa fa-folder-open"></i></button>
|
||||
<button ng-click="toggleShare()"><i class="fa fa-code"></i></button>
|
||||
<button ng-click="doVisualize()"><i class="fa fa-refresh"></i></button>
|
||||
</div>
|
||||
</navbar>
|
||||
|
||||
<config
|
||||
config-template="configTemplate"
|
||||
config-object="conf">
|
||||
</config>
|
||||
|
||||
<div class="vis-editor-content">
|
||||
<div class="vis-sidebar">
|
||||
<div class="sidebar-container">
|
||||
<form class="sidebar-list" ng-submit="doVisualize()" name="visualizeEditor">
|
||||
<ul class="list-unstyled">
|
||||
<li ng-repeat="category in visConfigCategories.displayOrder" class="sidebar-item">
|
||||
<vis-config-category
|
||||
vis="vis"
|
||||
category="vis[category.name]"
|
||||
fields="fields">
|
||||
</vis-config-category>
|
||||
</li>
|
||||
<li class="sidebar-item">
|
||||
<button
|
||||
ng-click="doVisualize()"
|
||||
ng-if="vis.dirty"
|
||||
ng-disabled="httpActive.length || visualizeEditor.$invalid"
|
||||
class="sidebar-item-button success">
|
||||
Apply
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vis-canvas">
|
||||
<vis-canvas><visualize vis="vis"></visualize></vis-canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="appEmbedded" class="container-fluid">
|
||||
<div class="row vis-editor-content">
|
||||
<div class="vis-canvas">
|
||||
<vis-canvas><visualize vis="vis"></visualize></vis-canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
79
src/kibana/apps/visualize/editor/agg.html
Normal file
79
src/kibana/apps/visualize/editor/agg.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
<ng-form name="aggForm" class="vis-editor-agg">
|
||||
|
||||
<!-- header -->
|
||||
<div class="vis-editor-agg-header">
|
||||
|
||||
<!-- open/close editor -->
|
||||
<button
|
||||
ng-click="editorOpen = !editorOpen"
|
||||
type="button"
|
||||
class="btn btn-xs vis-editor-agg-header-toggle">
|
||||
<i ng-class="{ 'fa-caret-down': editorOpen, 'fa-caret-right': !editorOpen }" class="fa"></i>
|
||||
</button>
|
||||
|
||||
<!-- title -->
|
||||
<span class="vis-editor-agg-header-title">
|
||||
{{ agg.schema.title }}
|
||||
</span>
|
||||
|
||||
<!-- description -->
|
||||
<span ng-if="!editorOpen && aggForm.$valid" class="vis-editor-agg-header-description">{{ describe() }}</span>
|
||||
|
||||
<!-- error -->
|
||||
<span ng-if="!editorOpen && aggForm.$invalid" class="vis-editor-agg-header-description danger">
|
||||
{{ describeError() }}
|
||||
</span>
|
||||
|
||||
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
|
||||
<div class="vis-editor-agg-header-controls btn-group">
|
||||
<button
|
||||
ng-if="group.length > 1"
|
||||
ng-class="{ disabled: $first }"
|
||||
ng-click="moveUp(agg)"
|
||||
tooltip="Increase Priority"
|
||||
tooltip-append-to-body="true"
|
||||
|
||||
type="button"
|
||||
class="btn btn-xs btn-default">
|
||||
<i class="fa fa-caret-up"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
ng-if="group.length > 1"
|
||||
ng-class="{ disabled: $last }"
|
||||
ng-click="moveDown(agg)"
|
||||
tooltip="Decrease Priority"
|
||||
tooltip-append-to-body="true"
|
||||
|
||||
type="button"
|
||||
class="btn btn-xs btn-default">
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
ng-if="group.length > groupMin"
|
||||
ng-click="remove(agg)"
|
||||
tooltip="Remove Dimension"
|
||||
tooltip-append-to-body="true"
|
||||
type="button"
|
||||
class="btn btn-xs btn-danger">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vis-editor-agg-editor" ng-show="editorOpen">
|
||||
<!-- schema editors go here -->
|
||||
|
||||
<div class="form-group">
|
||||
<label>Aggregation</label>
|
||||
<select
|
||||
name="agg"
|
||||
class="form-control"
|
||||
ng-model="agg.type"
|
||||
required
|
||||
ng-options="agg as agg.title for agg in aggTypeOptions">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
163
src/kibana/apps/visualize/editor/agg.js
Normal file
163
src/kibana/apps/visualize/editor/agg.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
define(function (require) {
|
||||
require('modules')
|
||||
.get('app/visualize')
|
||||
.directive('visEditorAgg', function ($compile, $parse, Private, Notifier) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var aggTypes = Private(require('components/agg_types/index'));
|
||||
|
||||
require('apps/visualize/editor/agg_param');
|
||||
|
||||
var notify = new Notifier({
|
||||
location: 'visAggGroup'
|
||||
});
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
template: require('text!apps/visualize/editor/agg.html'),
|
||||
scope: {
|
||||
vis: '=',
|
||||
agg: '=',
|
||||
$index: '=',
|
||||
group: '=',
|
||||
groupName: '=',
|
||||
groupMin: '='
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
$scope.aggTypeOptions = aggTypes.byType[$scope.groupName];
|
||||
$scope.editorOpen = $scope.agg.brandNew;
|
||||
|
||||
$scope.$watch('$index', function (i) {
|
||||
$scope.$first = i === 0;
|
||||
$scope.$last = i === $scope.group.length - 1;
|
||||
});
|
||||
|
||||
(function setupControlManagement() {
|
||||
var $editorContainer = $el.find('.vis-editor-agg-editor');
|
||||
|
||||
if ($scope.agg.schema.editor) {
|
||||
var $schemaEditor = $('<div>').prependTo($editorContainer);
|
||||
$schemaEditor.append($scope.agg.schema.editor);
|
||||
$compile($schemaEditor)(editorScope());
|
||||
}
|
||||
|
||||
var $aggParamEditors;
|
||||
var $aggParamEditorsScope;
|
||||
$scope.$watch('agg.type', function updateAggParamEditor() {
|
||||
if ($aggParamEditors) {
|
||||
$aggParamEditors.remove();
|
||||
$aggParamEditorsScope.$destroy();
|
||||
$aggParamEditors = $aggParamEditorsScope = null;
|
||||
}
|
||||
|
||||
var agg = $scope.agg;
|
||||
var type = $scope.agg.type;
|
||||
|
||||
if (!agg) return;
|
||||
agg.fillDefaults();
|
||||
|
||||
if (!type) return;
|
||||
|
||||
var editors = type.params.map(function (param, i) {
|
||||
if (!param.editor) return;
|
||||
|
||||
return $('<vis-agg-param-editor>')
|
||||
.attr({
|
||||
'agg-type': 'agg.type',
|
||||
'agg-config': 'agg',
|
||||
'agg-param': 'agg.type.params[' + i + ']',
|
||||
'params': 'agg.params'
|
||||
})
|
||||
.append(param.editor)
|
||||
.get(0);
|
||||
}).filter(Boolean);
|
||||
|
||||
$aggParamEditors = $(editors).appendTo($editorContainer);
|
||||
$aggParamEditorsScope = $scope.$new();
|
||||
$compile($aggParamEditors)($aggParamEditorsScope);
|
||||
});
|
||||
|
||||
// generic child scope creation, for both schema and agg
|
||||
function editorScope() {
|
||||
var $editorScope = $scope.$new();
|
||||
|
||||
setupBoundProp($editorScope, 'agg.type', 'aggType');
|
||||
setupBoundProp($editorScope, 'agg', 'aggConfig');
|
||||
setupBoundProp($editorScope, 'agg.params', 'params');
|
||||
|
||||
return $editorScope;
|
||||
}
|
||||
|
||||
// bind a property from our scope a child scope, with one-way binding
|
||||
function setupBoundProp($child, get, set) {
|
||||
var getter = _.partial($parse(get), $scope);
|
||||
var setter = _.partial($parse(set).assign, $child);
|
||||
$scope.$watch(getter, setter);
|
||||
}
|
||||
}());
|
||||
|
||||
/**
|
||||
* Describe the aggregation, for display in the collapsed agg header
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
$scope.describe = function () {
|
||||
if (!$scope.agg.type.makeLabel) return '';
|
||||
var label = $scope.agg.type.makeLabel($scope.agg);
|
||||
return label ? label : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Describe the errors in this agg
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
$scope.describeError = function () {
|
||||
var count = _.reduce($scope.aggForm.$error, function (count, controls, errorType) {
|
||||
return count + _.size(controls);
|
||||
}, 0);
|
||||
|
||||
return count + ' Error' + (count > 1 ? 's' : '');
|
||||
};
|
||||
|
||||
$scope.moveUp = function (agg) {
|
||||
var aggs = $scope.vis.aggs;
|
||||
|
||||
var i = aggs.indexOf(agg);
|
||||
if (i <= 0) return notify.log('already first');
|
||||
aggs.splice(i, 1);
|
||||
|
||||
// find the most previous bucket agg
|
||||
var d = i - 1;
|
||||
for (; d > 0 && aggs[d].schema.group !== 'buckets'; d--) ;
|
||||
|
||||
// place this right before
|
||||
aggs.splice(d, 0, agg);
|
||||
};
|
||||
|
||||
$scope.moveDown = function (agg) {
|
||||
var aggs = $scope.vis.aggs;
|
||||
|
||||
var i = aggs.indexOf(agg);
|
||||
if (i >= aggs.length - 1) return notify.log('already last');
|
||||
aggs.splice(i, 1);
|
||||
|
||||
// find the next bucket agg
|
||||
var d = i;
|
||||
for (; d < aggs.length && aggs[d].schema.group !== 'buckets'; d++) ;
|
||||
|
||||
// place this agg right after
|
||||
aggs.splice(d + 1, 0, agg);
|
||||
};
|
||||
|
||||
$scope.remove = function (agg) {
|
||||
var aggs = $scope.vis.aggs;
|
||||
|
||||
var index = aggs.indexOf(agg);
|
||||
if (index === -1) return notify.log('already removed');
|
||||
|
||||
aggs.splice(index, 1);
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
48
src/kibana/apps/visualize/editor/agg_group.html
Normal file
48
src/kibana/apps/visualize/editor/agg_group.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
<li class="sidebar-item">
|
||||
<div class="sidebar-item-title">
|
||||
{{ groupName }}
|
||||
</div>
|
||||
|
||||
<div class="vis-editor-agg-group" ng-class="groupName">
|
||||
<!-- wrapper needed for nesting-indicator -->
|
||||
<div ng-repeat="agg in group" class="vis-editor-agg-wrapper">
|
||||
<nesting-indicator
|
||||
ng-if="groupName === 'buckets'"
|
||||
item="agg"
|
||||
index="$index"
|
||||
list="group">
|
||||
</nesting-indicator>
|
||||
|
||||
<!-- agg.html -->
|
||||
<vis-editor-agg
|
||||
vis="vis"
|
||||
group="group"
|
||||
group-name="groupName"
|
||||
group-min="stats.min"
|
||||
$index="$index"
|
||||
agg="agg">
|
||||
</vis-editor-agg>
|
||||
</div>
|
||||
|
||||
<div ng-if="addForm.visible" class="vis-editor-agg-add-form form-group">
|
||||
<label>Select {{ groupName }} type</label>
|
||||
<button
|
||||
ng-repeat="schema in availableSchema"
|
||||
ng-click="createUsingSchema(schema)"
|
||||
class="btn-default">
|
||||
<i ng-show="schema.icon" ng-class="schema.icon"></i>
|
||||
{{schema.title}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
ng-if="stats.count < stats.max"
|
||||
type="button"
|
||||
ng-click="addForm.visible = !addForm.visible"
|
||||
class="vis-editor-agg-wide-btn btn btn-xs btn-default" >
|
||||
|
||||
<i ng-if="!addForm.visible" class="fa fa-plus"></i>
|
||||
<span ng-if="addForm.visible">cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
72
src/kibana/apps/visualize/editor/agg_group.js
Normal file
72
src/kibana/apps/visualize/editor/agg_group.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
define(function (require) {
|
||||
require('modules')
|
||||
.get('app/visualize')
|
||||
.directive('visEditorAggGroup', function (Private) {
|
||||
require('apps/visualize/editor/agg');
|
||||
require('apps/visualize/editor/nesting_indicator');
|
||||
|
||||
var eachGroupHtml = require('text!apps/visualize/editor/agg_group.html');
|
||||
var AggConfig = Private(require('components/vis/_agg_config'));
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: require('text!apps/visualize/editor/agg_group.html'),
|
||||
replace: true,
|
||||
scope: {
|
||||
vis: '=',
|
||||
schemas: '=',
|
||||
group: '=',
|
||||
groupName: '='
|
||||
},
|
||||
link: function ($scope) {
|
||||
|
||||
// "sub-scope" for the add form to use
|
||||
$scope.addForm = {};
|
||||
|
||||
$scope.$watchMulti([
|
||||
'schemas',
|
||||
'group.length'
|
||||
], function () {
|
||||
var stats = $scope.stats = {
|
||||
min: 0,
|
||||
max: 0,
|
||||
count: $scope.group ? $scope.group.length : 0
|
||||
};
|
||||
|
||||
if (!$scope.schemas) return;
|
||||
|
||||
$scope.schemas.forEach(function (schema) {
|
||||
stats.min += schema.min;
|
||||
stats.max += schema.max;
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watchCollection('group', function () {
|
||||
$scope.availableSchema = $scope.schemas.filter(function (schema) {
|
||||
var count = 0;
|
||||
|
||||
if ($scope.group) {
|
||||
count = $scope.group.reduce(function (count, aggConfig) {
|
||||
if (aggConfig.schema === schema) count += 1;
|
||||
return count;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (count < schema.max) return true;
|
||||
});
|
||||
});
|
||||
|
||||
$scope.createUsingSchema = function (schema) {
|
||||
$scope.addForm = {};
|
||||
|
||||
var aggConfig = new AggConfig($scope.vis, {
|
||||
schema: schema
|
||||
});
|
||||
aggConfig.brandNew = true;
|
||||
$scope.vis.aggs.push(aggConfig);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
});
|
18
src/kibana/apps/visualize/editor/agg_param.js
Normal file
18
src/kibana/apps/visualize/editor/agg_param.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
define(function (require) {
|
||||
require('modules')
|
||||
.get('app/visualize')
|
||||
.directive('visAggParamEditor', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
aggType: '=',
|
||||
aggConfig: '=',
|
||||
aggParam: '=',
|
||||
params: '='
|
||||
},
|
||||
template: function ($el, attr) {
|
||||
return $el.html();
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
87
src/kibana/apps/visualize/editor/editor.html
Normal file
87
src/kibana/apps/visualize/editor/editor.html
Normal file
|
@ -0,0 +1,87 @@
|
|||
<div ng-controller="VisEditor" class="vis-editor">
|
||||
|
||||
<navbar ng-if="!appEmbedded">
|
||||
<div class="fill bitty-modal-container">
|
||||
<div ng-if="linked && !unlinking"
|
||||
ng-dblclick="unlink()"
|
||||
tooltip="Double click to unlink this visualization from the saved search"
|
||||
class="bitty-modal visualize-linked">
|
||||
<i class="fa fa-link"></i>
|
||||
|
||||
This visualization is linked to a saved search:
|
||||
<b>{{ savedVis.savedSearchId | json}}</b>
|
||||
<a href="#/discover/{{ savedVis.savedSearchId | uriescape }}">
|
||||
<i class="fa fa-pencil" tooltip="Click here to edit the linked saved search"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="linked && unlinking" ng-click="doneUnlinking()" class="bitty-modal">
|
||||
<i class="fa fa-chain-broken"></i> Unlinked!
|
||||
</div>
|
||||
|
||||
<form ng-submit="fetch()" class="inline-form" name="queryInput">
|
||||
<div class="typeahead" kbn-typeahead="visualize">
|
||||
<div class="input-group"
|
||||
ng-class="queryInput.$invalid ? 'has-error' : ''">
|
||||
<input
|
||||
query-input="savedVis.searchSource"
|
||||
input-focus
|
||||
kbn-typeahead-input
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="state.query">
|
||||
|
||||
<button
|
||||
class="btn btn-default" type="submit"
|
||||
ng-disabled="queryInput.$invalid">
|
||||
<span class="fa fa-search"></span>
|
||||
</button>
|
||||
</div>
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button ng-click="startOver()"><i class="fa fa-file-o"></i></button>
|
||||
|
||||
<!-- normal save -->
|
||||
<button ng-if="!editableVis.dirty" ng-click="toggleSave()">
|
||||
<i class="fa fa-save"></i>
|
||||
</button>
|
||||
|
||||
<!-- save stub with tooltip -->
|
||||
<button disabled ng-if="editableVis.dirty" tooltip="Apply or Discard your changes before saving">
|
||||
<i class="fa fa-save"></i>
|
||||
</button>
|
||||
|
||||
<button ng-click="toggleLoad()"><i class="fa fa-folder-open"></i></button>
|
||||
<button ng-click="toggleShare()"><i class="fa fa-code"></i></button>
|
||||
<button ng-click="fetch()"><i class="fa fa-refresh"></i></button>
|
||||
</div>
|
||||
</navbar>
|
||||
|
||||
|
||||
<config
|
||||
ng-if="!appEmbedded"
|
||||
config-template="configTemplate"
|
||||
config-object="conf">
|
||||
</config>
|
||||
|
||||
|
||||
<div class="vis-editor-content">
|
||||
<vis-editor-sidebar
|
||||
ng-if="!appEmbedded"
|
||||
vis="editableVis"
|
||||
apply="stageEditableVis(); fetch();"
|
||||
reset="resetEditableVis();">
|
||||
</vis-editor-sidebar>
|
||||
|
||||
<div class="vis-editor-canvas">
|
||||
<p class="vis-editor-canvas-title" ng-if="savedVis.title" ng-bind="savedVis.title"></p>
|
||||
<visualize vis="vis" search-source="savedVis.searchSource"></visualize>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
208
src/kibana/apps/visualize/editor/editor.js
Normal file
208
src/kibana/apps/visualize/editor/editor.js
Normal file
|
@ -0,0 +1,208 @@
|
|||
define(function (require) {
|
||||
require('apps/visualize/saved_visualizations/saved_visualizations');
|
||||
require('apps/visualize/editor/sidebar');
|
||||
|
||||
require('directives/saved_object_finder');
|
||||
require('components/visualize/visualize');
|
||||
require('filters/uriescape');
|
||||
|
||||
require('routes')
|
||||
.when('/visualize/create', {
|
||||
template: require('text!apps/visualize/editor/editor.html'),
|
||||
resolve: {
|
||||
savedVis: function (savedVisualizations, courier, $route) {
|
||||
if (!$route.current.params.indexPattern && !$route.current.params.savedSearchId) {
|
||||
throw new Error('You must provide either an indexPattern or a savedSearchId');
|
||||
}
|
||||
|
||||
return savedVisualizations.get($route.current.params)
|
||||
.catch(courier.redirectWhenMissing({
|
||||
//'index-pattern': '/visualize',
|
||||
'*': '/visualize'
|
||||
}));
|
||||
}
|
||||
}
|
||||
})
|
||||
.when('/visualize/edit/:id', {
|
||||
template: require('text!apps/visualize/editor/editor.html'),
|
||||
resolve: {
|
||||
savedVis: function (savedVisualizations, courier, $route) {
|
||||
return savedVisualizations.get($route.current.params.id)
|
||||
.catch(courier.redirectWhenMissing({
|
||||
'index-pattern': '/settings',
|
||||
'*': '/visualize'
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
require('modules')
|
||||
.get('app/visualize', [
|
||||
'kibana/notify',
|
||||
'kibana/courier'
|
||||
])
|
||||
.controller('VisEditor', function ($scope, $route, timefilter, appStateFactory, $location, globalState, $timeout) {
|
||||
|
||||
var _ = require('lodash');
|
||||
var angular = require('angular');
|
||||
var ConfigTemplate = require('utils/config_template');
|
||||
var Notifier = require('components/notify/_notifier');
|
||||
|
||||
var notify = new Notifier({
|
||||
location: 'Visualization Editor'
|
||||
});
|
||||
|
||||
var savedVis = $route.current.locals.savedVis;
|
||||
var vis = savedVis.vis;
|
||||
var editableVis = vis.clone();
|
||||
var searchSource = savedVis.searchSource;
|
||||
|
||||
// config panel templates
|
||||
var configTemplate = new ConfigTemplate({
|
||||
save: require('text!apps/visualize/editor/panels/save.html'),
|
||||
load: require('text!apps/visualize/editor/panels/load.html'),
|
||||
share: require('text!apps/visualize/editor/panels/share.html'),
|
||||
});
|
||||
|
||||
var $state = (function initState() {
|
||||
var savedVisState = vis.getState();
|
||||
|
||||
var $state = appStateFactory.create({
|
||||
vis: savedVisState
|
||||
});
|
||||
|
||||
if (!angular.equals($state.vis, savedVisState)) {
|
||||
vis.setState($state.vis);
|
||||
editableVis.setState($state.vis);
|
||||
}
|
||||
|
||||
return $state;
|
||||
}());
|
||||
|
||||
function init() {
|
||||
// export some objects
|
||||
$scope.savedVis = savedVis;
|
||||
$scope.vis = vis;
|
||||
$scope.editableVis = editableVis;
|
||||
$scope.state = $state;
|
||||
|
||||
$scope.conf = _.pick($scope, 'doSave', 'savedVis', 'shareData');
|
||||
$scope.configTemplate = configTemplate;
|
||||
$scope.toggleShare = _.bindKey(configTemplate, 'toggle', 'share');
|
||||
$scope.toggleSave = _.bindKey(configTemplate, 'toggle', 'save');
|
||||
$scope.toggleLoad = _.bindKey(configTemplate, 'toggle', 'load');
|
||||
|
||||
$scope.linked = !!savedVis.savedSearchId;
|
||||
if ($scope.linked) {
|
||||
// possibly left over state from unsaved unlinking
|
||||
delete $state.query;
|
||||
} else {
|
||||
$state.query = $state.query || searchSource.get('query');
|
||||
searchSource.set('query', $state.query);
|
||||
}
|
||||
|
||||
// track state of editable vis vs. "actual" vis
|
||||
$scope.stageEditableVis = transferVisState(editableVis, vis);
|
||||
$scope.resetEditableVis = transferVisState(vis, editableVis);
|
||||
$scope.$watch(function () {
|
||||
return editableVis.getState();
|
||||
}, function (newState) {
|
||||
editableVis.dirty = !angular.equals(newState, vis.getState());
|
||||
}, true);
|
||||
|
||||
$state.on('fetch_with_changes', function () {
|
||||
vis.setState($state.vis);
|
||||
editableVis.setState($state.vis);
|
||||
|
||||
// we use state to track query, must write before we fetch
|
||||
if ($state.query) {
|
||||
searchSource.set('query', $state.query);
|
||||
} else {
|
||||
searchSource.set('query', null);
|
||||
}
|
||||
|
||||
$scope.fetch();
|
||||
});
|
||||
|
||||
timefilter.enabled = true;
|
||||
timefilter.on('update', _.bindKey($scope, 'fetch'));
|
||||
|
||||
$scope.$on('ready:vis', function () {
|
||||
$scope.$emit('application.load');
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
savedVis.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.fetch = function () {
|
||||
searchSource.fetch();
|
||||
};
|
||||
|
||||
$scope.startOver = function () {
|
||||
$location.url('/visualize');
|
||||
};
|
||||
|
||||
$scope.doSave = function () {
|
||||
savedVis.id = savedVis.title;
|
||||
savedVis.visState = $state.vis;
|
||||
|
||||
savedVis.save()
|
||||
.then(function () {
|
||||
configTemplate.close('save');
|
||||
notify.info('Saved Visualization "' + savedVis.title + '"');
|
||||
|
||||
if (savedVis.id === $route.current.params.id) return;
|
||||
|
||||
$location.url(
|
||||
globalState.writeToUrl(
|
||||
'/visualize/edit/' + encodeURIComponent(savedVis.id)
|
||||
)
|
||||
);
|
||||
}, notify.fatal);
|
||||
};
|
||||
|
||||
$scope.shareData = function () {
|
||||
return {
|
||||
link: $location.absUrl(),
|
||||
// This sucks, but seems like the cleanest way. Uhg.
|
||||
embed: $location.absUrl().replace('?', '?embed&')
|
||||
};
|
||||
};
|
||||
|
||||
$scope.unlink = function () {
|
||||
// display unlinking for 2 seconds, unless it is double clicked
|
||||
$scope.unlinking = $timeout($scope.doneUnlinking, 2000);
|
||||
|
||||
delete savedVis.savedSearchId;
|
||||
|
||||
var q = searchSource.get('query');
|
||||
$state.query = q;
|
||||
|
||||
var parent = searchSource.parent();
|
||||
// we will copy over all state minus the "aggs"
|
||||
_(parent.toJSON()).omit('aggs').forOwn(function (val, key) {
|
||||
searchSource.set(key, val);
|
||||
});
|
||||
|
||||
searchSource.inherits(parent.parent());
|
||||
};
|
||||
|
||||
$scope.doneUnlinking = function () {
|
||||
$scope.unlinking = clearTimeout($scope.unlinking);
|
||||
$scope.linked = false;
|
||||
};
|
||||
|
||||
function transferVisState(fromVis, toVis) {
|
||||
return function () {
|
||||
toVis.setState(fromVis.getState());
|
||||
editableVis.dirty = false;
|
||||
$state.vis = vis.getState();
|
||||
$state.save();
|
||||
};
|
||||
}
|
||||
|
||||
init();
|
||||
});
|
||||
});
|
202
src/kibana/apps/visualize/editor/editor.less
Normal file
202
src/kibana/apps/visualize/editor/editor.less
Normal file
|
@ -0,0 +1,202 @@
|
|||
.vis-editor {
|
||||
.flex-parent();
|
||||
|
||||
navbar {
|
||||
.bitty-modal-container {
|
||||
position: relative;
|
||||
|
||||
.bitty-modal {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
z-index: 10;
|
||||
background: rgba(70, 82, 93, 0.9);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding-top: 6px;
|
||||
.user-select(none);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
.flex-parent();
|
||||
|
||||
// overrides for tablet and desktop
|
||||
@media (min-width: @screen-md-min) {
|
||||
.flex-direction(row);
|
||||
}
|
||||
}
|
||||
|
||||
&-sidebar {
|
||||
.flex-parent(0, 0, auto);
|
||||
overflow: auto;
|
||||
|
||||
// overrided for tablet and desktop
|
||||
@media (min-width: @screen-md-min) {
|
||||
.flex-basis(@vis-editor-sidebar-basis);
|
||||
min-width: @vis-editor-sidebar-min-width;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
.flex(1, 0, auto);
|
||||
background-color: @body-bg;
|
||||
border-right-color: @sidebar-bg;
|
||||
}
|
||||
.sidebar-item-title {
|
||||
background: @sidebar-bg;
|
||||
}
|
||||
.sidebar-item-title:hover {
|
||||
color: @sidebar-header-color !important;
|
||||
background-color: @sidebar-bg !important;
|
||||
}
|
||||
}
|
||||
|
||||
nesting-indicator {
|
||||
.display(flex);
|
||||
.flex(0 0 auto);
|
||||
|
||||
> span {
|
||||
width: @vis-editor-nesting-width;
|
||||
background-color: @brand-success;
|
||||
|
||||
.transition(width .3s ease-out);
|
||||
|
||||
&.expand {
|
||||
width: @vis-editor-nesting-expand-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-agg {
|
||||
.flex-parent();
|
||||
|
||||
padding: @vis-editor-agg-editor-spacing;
|
||||
border-bottom: 1px solid @sidebar-bg;
|
||||
|
||||
// wraps the .vis-editor-agg and nesting-indicator ^^
|
||||
&-wrapper {
|
||||
.display(flex);
|
||||
}
|
||||
|
||||
&-group {
|
||||
.flex-parent();
|
||||
color: @text-color;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
.display(flex);
|
||||
.align-items(center);
|
||||
.flex(1);
|
||||
|
||||
&-toggle {
|
||||
.flex(0, 0, auto);
|
||||
margin-right: @vis-editor-agg-editor-spacing;
|
||||
}
|
||||
|
||||
&-title {
|
||||
.flex(1, 1, auto);
|
||||
.ellipsis();
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-description {
|
||||
font-weight: normal;
|
||||
padding-right: @vis-editor-agg-editor-spacing;
|
||||
|
||||
&.danger {
|
||||
.text-danger();
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&-controls {
|
||||
.flex(0, 0, auto);
|
||||
}
|
||||
}
|
||||
|
||||
&-editor {
|
||||
margin-top: @vis-editor-agg-editor-spacing;
|
||||
|
||||
&-ranges {
|
||||
td {
|
||||
padding: 0 @vis-editor-agg-editor-spacing @vis-editor-agg-editor-spacing 0;
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-form-row {
|
||||
.display(flex);
|
||||
|
||||
> * {
|
||||
.flex(1, 1, auto);
|
||||
margin-right: @vis-editor-agg-editor-spacing;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
> .btn {
|
||||
.align-self(center);
|
||||
}
|
||||
}
|
||||
|
||||
&-wide-btn {
|
||||
.border-radius(0);
|
||||
}
|
||||
|
||||
&-add-form {
|
||||
margin: @vis-editor-agg-editor-spacing * 3;
|
||||
padding: @vis-editor-agg-editor-spacing;
|
||||
> button {
|
||||
display: block;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-canvas {
|
||||
.flex(1, 0, @screen-md-min - @vis-editor-sidebar-basis);
|
||||
.display(flex);
|
||||
.flex-direction(column);
|
||||
overflow: auto;
|
||||
|
||||
// overrided for tablet and desktop
|
||||
@media (min-width: @screen-md-min) {
|
||||
.flex-shrink(1);
|
||||
.flex-basis(100%);
|
||||
}
|
||||
|
||||
&-title {
|
||||
text-align: center;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
visualize {
|
||||
.flex-parent();
|
||||
.flex(1, 0, auto);
|
||||
}
|
||||
|
||||
.visualize-chart {
|
||||
.flex(1, 0, 100%);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
form.vis-share {
|
||||
div.form-control {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
90
src/kibana/apps/visualize/editor/nesting_indicator.js
Normal file
90
src/kibana/apps/visualize/editor/nesting_indicator.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
define(function (require) {
|
||||
require('modules')
|
||||
.get('kibana')
|
||||
.directive('nestingIndicator', function ($rootScope, $parse) {
|
||||
var _ = require('lodash');
|
||||
var angular = require('angular');
|
||||
var ruleBase = 'border-left-';
|
||||
|
||||
var getColor = (function () {
|
||||
var i = 0;
|
||||
var colorPool = require('components/vislib/utils/colorspace')(100);
|
||||
var assigned = {};
|
||||
return function (item) {
|
||||
var key = item.$$hashKey;
|
||||
if (!key) throw new Error('expected an item that is part of an ngRepeat');
|
||||
|
||||
if (!assigned[key]) {
|
||||
assigned[key] = colorPool[i++ % colorPool.length];
|
||||
}
|
||||
|
||||
return assigned[key];
|
||||
};
|
||||
}());
|
||||
|
||||
var allIndicators = [];
|
||||
allIndicators.expanded = false;
|
||||
allIndicators.expand = toggler(true);
|
||||
allIndicators.contract = toggler(false, 150);
|
||||
|
||||
function toggler(on, delay) {
|
||||
var all = allIndicators;
|
||||
var work = function () {
|
||||
if (delay && all.expanded !== on) return;
|
||||
all.forEach(function ($scope) {
|
||||
if (!$scope.bars) return;
|
||||
$scope.bars.forEach(function ($el) {
|
||||
$el.toggleClass('expand', on);
|
||||
});
|
||||
});
|
||||
};
|
||||
return function () {
|
||||
all.expanded = on;
|
||||
if (!delay) work();
|
||||
else setTimeout(work, delay);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
item: '=',
|
||||
list: '='
|
||||
},
|
||||
link: function ($scope, $el, attr) {
|
||||
|
||||
allIndicators.push($scope);
|
||||
$el.on('mouseenter', allIndicators.expand);
|
||||
$el.on('mouseleave', allIndicators.contract);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
_.pull(allIndicators, $scope);
|
||||
$el.off('mouseenter', allIndicators.expand);
|
||||
$el.off('mouseleave', allIndicators.contract);
|
||||
});
|
||||
|
||||
$scope.$watchCollection('list', function () {
|
||||
if (!$scope.list || !$scope.item) return;
|
||||
|
||||
var item = $scope.item;
|
||||
var list = $scope.list;
|
||||
var bars = $scope.bars = [];
|
||||
|
||||
for (var i = 0; i <= list.length; i++) {
|
||||
var color = getColor(list[i]);
|
||||
|
||||
bars.push(
|
||||
angular
|
||||
.element('<span>')
|
||||
.css('background-color', color)
|
||||
);
|
||||
|
||||
if (list[i] === $scope.item) break;
|
||||
}
|
||||
|
||||
$el.html(bars);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
<form role="form" ng-submit="conf.doSave()">
|
||||
<div class="form-group">
|
||||
<label for="visTitle">Title</label>
|
||||
<input class="form-control" type="text" name="visTitle" ng-model="conf.vis.title" required input-focus>
|
||||
<input class="form-control" type="text" name="visTitle" ng-model="conf.savedVis.title" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="visDescription">Description</label>
|
||||
<textarea class="form-control" name="visDescription" ng-model="conf.vis.description" placeholder=""></textarea>
|
||||
<textarea class="form-control" name="visDescription" ng-model="conf.savedVis.description" placeholder=""></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
11
src/kibana/apps/visualize/editor/panels/share.html
Normal file
11
src/kibana/apps/visualize/editor/panels/share.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<form role="form" class="vis-share">
|
||||
<div class="form-group">
|
||||
<label>Embed this visualization. <small>Copy code into your html source. Note all clients must still be able to access kibana</small></label>
|
||||
<div class="form-control" disabled><iframe src="{{conf.shareData().embed}}" height="400" width="600"></iframe></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Share a link</label>
|
||||
<div class="form-control" disabled>{{conf.shareData().link}}</div>
|
||||
</div>
|
||||
</form>
|
46
src/kibana/apps/visualize/editor/sidebar.html
Normal file
46
src/kibana/apps/visualize/editor/sidebar.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<div class="vis-editor-sidebar">
|
||||
<div class="sidebar-container">
|
||||
<form class="sidebar-list" ng-submit="apply()" name="visualizeEditor">
|
||||
<ul class="list-unstyled">
|
||||
|
||||
<!-- metrics -->
|
||||
<vis-editor-agg-group
|
||||
ng-if="vis.type.schemas.metrics"
|
||||
vis="vis"
|
||||
saved-vis="savedVis"
|
||||
schemas="vis.type.schemas.metrics"
|
||||
group="vis.aggs.bySchemaGroup.metrics"
|
||||
group-name="'metrics'"
|
||||
>
|
||||
</vis-editor-agg-group>
|
||||
|
||||
<!-- buckets -->
|
||||
<vis-editor-agg-group
|
||||
ng-if="vis.type.schemas.buckets"
|
||||
vis="vis"
|
||||
saved-vis="savedVis"
|
||||
schemas="vis.type.schemas.buckets"
|
||||
group="vis.aggs.bySchemaGroup.buckets"
|
||||
group-name="'buckets'"
|
||||
>
|
||||
</vis-editor-agg-group>
|
||||
|
||||
<!-- apply/discard -->
|
||||
<li ng-if="vis.dirty" class="sidebar-item">
|
||||
<button
|
||||
type="submit"
|
||||
ng-disabled="httpActive.length || visualizeEditor.$invalid"
|
||||
class="sidebar-item-button success">
|
||||
Apply
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
ng-click="reset()"
|
||||
class="sidebar-item-button warn">
|
||||
Discard
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
21
src/kibana/apps/visualize/editor/sidebar.js
Normal file
21
src/kibana/apps/visualize/editor/sidebar.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
define(function (require) {
|
||||
require('modules')
|
||||
.get('app/visualize')
|
||||
.directive('visEditorSidebar', function () {
|
||||
var _ = require('lodash');
|
||||
|
||||
require('apps/visualize/editor/agg_group');
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: require('text!apps/visualize/editor/sidebar.html'),
|
||||
replace: true,
|
||||
scope: {
|
||||
vis: '=',
|
||||
savedVis: '=',
|
||||
apply: '&',
|
||||
reset: '&'
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
Binary file not shown.
Before Width: | Height: | Size: 9.6 KiB |
|
@ -1,13 +1,8 @@
|
|||
define(function (require) {
|
||||
require('css!apps/visualize/styles/main.css');
|
||||
|
||||
require('apps/visualize/controllers/editor');
|
||||
require('apps/visualize/controllers/wizard');
|
||||
|
||||
require('apps/visualize/directives/canvas');
|
||||
require('apps/visualize/directives/visualize');
|
||||
require('apps/visualize/directives/config_category');
|
||||
require('apps/visualize/directives/search_editor');
|
||||
require('apps/visualize/editor/editor');
|
||||
require('apps/visualize/wizard/wizard');
|
||||
|
||||
require('routes')
|
||||
.when('/visualize', {
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<div
|
||||
class="sidebar-item-title"
|
||||
ng-if="category.name !== 'group' || vis.segment.configs.length > 0">
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
ng-if="category.configs.length < category.max"
|
||||
ng-click="vis.addConfig(category.name)"
|
||||
class="btn btn-xs btn-default" >
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
{{ category.label }}
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="category.configs.length > 0" class="vis-config-details">
|
||||
<vis-config-editor
|
||||
ng-repeat="config in category.configs"
|
||||
category="category"
|
||||
config="config"
|
||||
vis="vis"
|
||||
fields="fields"
|
||||
move="moveHandler">
|
||||
</vis-config-editor>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label>Min <small>(optional)</small></label>
|
||||
<input
|
||||
ng-show="aggParams.min_doc_count"
|
||||
ng-model="config.extended_bounds.min"
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="extended_bounds.min" />
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label>Max <small>(optional)</small></label>
|
||||
<input
|
||||
ng-show="aggParams.min_doc_count"
|
||||
ng-model="config.extended_bounds.max"
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="extended_bounds.max" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,26 +0,0 @@
|
|||
<div ng-init="config.filters = (config.filters || [{input: {}}])">
|
||||
<div class="form-group" >
|
||||
<div ng-repeat="filter in config.filters track by $index">
|
||||
<div class="config-controls pull-right btn-group">
|
||||
<span ng-click="config.filters.splice($index, 1)"
|
||||
class="btn btn-danger btn-xs ">
|
||||
<i class="fa fa-ban" ></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Query string {{$index + 1}}</label>
|
||||
<input query-input
|
||||
ng-model="filter.input"
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="filter{{$index}}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ng-click="config.filters.push({input: {}})"
|
||||
class="sidebar-item-button primary">
|
||||
Add filter
|
||||
</div>
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
ng-model="config.global"
|
||||
type="checkbox">Run First
|
||||
|
||||
<kbn-info info="Ensure that all charts have the same {{group.label | lowercase}} values." placement="right"></kbn-info>
|
||||
</label>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
<div class="form-group">
|
||||
<table class="agg-config-interval">
|
||||
<tr>
|
||||
<td>
|
||||
<label>Interval</label>
|
||||
<select
|
||||
ng-if="aggParams.interval.options"
|
||||
ng-model="config.interval"
|
||||
ng-options="opt.val as opt.display for opt in aggParams.interval.options"
|
||||
class="form-control"
|
||||
name="interval">
|
||||
</select>
|
||||
<input
|
||||
ng-if="!aggParams.interval.options"
|
||||
ng-model="config.interval"
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="interval" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
|
@ -1,43 +0,0 @@
|
|||
<div ng-init="config.ranges = (config.ranges || [{}])">
|
||||
<div class="form-group" >
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-5">
|
||||
<label>From</label>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<label>To</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row"
|
||||
ng-repeat="range in config.ranges track by $index">
|
||||
<div class="col-xs-5">
|
||||
<input validate-ip
|
||||
ng-model="range.from"
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="range.from" />
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<input validate-ip
|
||||
ng-model="range.to"
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="range.to" />
|
||||
</div>
|
||||
<div class="col-xs-1">
|
||||
<button ng-click="config.ranges.splice($index, 1)"
|
||||
class="btn btn-danger btn-xs">
|
||||
<i class="fa fa-ban" ></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
ng-click="config.ranges.push({})"
|
||||
class="sidebar-item-button primary">
|
||||
Add Range
|
||||
</div>
|
||||
</div>
|
|
@ -1,16 +0,0 @@
|
|||
<div class="form-group">
|
||||
<table class="agg-config-interval">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="checkbox ng-scope">
|
||||
<label>
|
||||
<input ng-model="config.min_doc_count"
|
||||
type="checkbox">Show empty buckets
|
||||
|
||||
<kbn-info info="Show all buckets, not only the buckets with results." placement="right" class="ng-isolate-scope"></kbn-info>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
|
@ -1,15 +0,0 @@
|
|||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<select
|
||||
class="form-control"
|
||||
name="order"
|
||||
ng-model="config.order"
|
||||
ng-options="opt.val as opt.display for opt in aggParams.order.options">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="number" ng-model="config.size" name="size">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,43 +0,0 @@
|
|||
<div ng-init="config.ranges = (config.ranges || [{}])">
|
||||
<div class="form-group" >
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-5">
|
||||
<label>From</label>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<label>To</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row"
|
||||
ng-repeat="range in config.ranges track by $index">
|
||||
<div class="col-xs-5">
|
||||
<input
|
||||
ng-model="range.from"
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="range.from" />
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<input
|
||||
ng-model="range.to"
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="range.to" />
|
||||
</div>
|
||||
<div class="col-xs-1">
|
||||
<button ng-click="config.ranges.splice($index, 1)"
|
||||
class="btn btn-danger btn-xs">
|
||||
<i class="fa fa-ban" ></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
ng-click="config.ranges.push({})"
|
||||
class="sidebar-item-button primary">
|
||||
Add Range
|
||||
</div>
|
||||
</div>
|
|
@ -1,39 +0,0 @@
|
|||
<div ng-if="category.name === 'split'" class="form-group">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-default"
|
||||
ng-model="config.row"
|
||||
btn-radio="true">
|
||||
Row
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-default"
|
||||
ng-model="config.row"
|
||||
btn-radio="false">
|
||||
Column
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="availableAggs">
|
||||
<label>Aggregation</label>
|
||||
<select
|
||||
name="agg"
|
||||
class="form-control"
|
||||
ng-model="config.agg"
|
||||
ng-options="agg.name as agg.display for agg in availableAggs">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="field">
|
||||
Field
|
||||
</label>
|
||||
<select
|
||||
class="form-control"
|
||||
name="field"
|
||||
ng-model="config.field"
|
||||
ng-options="field.name as field.name group by field.type for field in fields | filter:{indexed:true} |orderBy:['type','name']">
|
||||
</select>
|
||||
</div>
|
||||
<div class="agg-param-controls"></div>
|
|
@ -1,25 +0,0 @@
|
|||
<div class="config-controls pull-right btn-group">
|
||||
<button
|
||||
ng-if="category.configs.length > 1"
|
||||
ng-click="move(config, -1)"
|
||||
type="button"
|
||||
ng-disabled="category.configs.indexOf(config) < 1"
|
||||
class="btn btn-xs btn-default">
|
||||
<div tooltip="Increase Priority"><i class="fa fa-caret-up"></i></div>
|
||||
</button>
|
||||
<button
|
||||
ng-if="category.configs.length > 1"
|
||||
ng-click="move(config, 1)"
|
||||
type="button"
|
||||
ng-disabled="category.configs.indexOf(config) >= category.configs.length - 1"
|
||||
class="btn btn-xs btn-default">
|
||||
<div tooltip="Decrease Priority"><i class="fa fa-caret-down"></i></div>
|
||||
</button>
|
||||
<button
|
||||
ng-if="category.configs.length > category.min"
|
||||
ng-click="move(config, false)"
|
||||
type="button"
|
||||
class="btn btn-xs btn-danger">
|
||||
<div tooltip="Remove Dimension"><i class="fa fa-times"></i></div>
|
||||
</button>
|
||||
</div>
|
|
@ -1,21 +0,0 @@
|
|||
<div class="form-group">
|
||||
<label for="stat">Stat</label>
|
||||
<select
|
||||
class="form-control"
|
||||
name="stat"
|
||||
ng-model="config.agg"
|
||||
ng-options="agg.name as agg.name for agg in aggs.metricAggs">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-if="config.agg && config.agg !== 'count'">
|
||||
<label for="field">
|
||||
Field to {{config.agg}}
|
||||
<kbn-info placement="right" info="Field to use for the {{metric.label}}"></kbn-info>
|
||||
</label>
|
||||
<select
|
||||
class="form-control"
|
||||
name="field"
|
||||
ng-model="config.field"
|
||||
ng-options="field.name as field.name for field in fields | fieldType:aggs.metricAggsByName[config.agg].types">
|
||||
</select>
|
||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||
<form role="form" class="vis-share">
|
||||
<div class="form-group">
|
||||
<label>Embed this visualization. <small>Copy code into your html source. Note all clients must still be able to access kibana</small></label>
|
||||
<div class="form-control" disabled><iframe src="{{conf.shareData().embed}}" height="400" width="600"></iframe></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Share a link</label>
|
||||
<div class="form-control" disabled>{{conf.shareData().link}}</div>
|
||||
</div>
|
||||
</form>
|
|
@ -1,131 +0,0 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
|
||||
var module = require('modules').get('app/visualize');
|
||||
var configCats = require('apps/visualize/saved_visualizations/_config_categories');
|
||||
|
||||
|
||||
module.factory('AdhocVis', function (courier, Private, Promise) {
|
||||
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
|
||||
|
||||
/**
|
||||
opts params:
|
||||
{
|
||||
type: 'histogram', // The chart type
|
||||
listeners : {
|
||||
onClick: function,
|
||||
onHover: function,
|
||||
onBrush: function,
|
||||
},
|
||||
params: {}, // top level chart parameters
|
||||
searchSource: SearchSource // the search source for the visualization
|
||||
}
|
||||
|
||||
*/
|
||||
function AdhocVis(opts) {
|
||||
opts = opts || {};
|
||||
if (!_.isObject(opts)) throw new TypeError('options must be an object');
|
||||
|
||||
var vis = this;
|
||||
var params;
|
||||
|
||||
var createdSource = true;
|
||||
|
||||
vis.init = _.once(function () {
|
||||
vis.typeName = opts.type || 'histogram';
|
||||
vis.params = _.cloneDeep(opts.params);
|
||||
|
||||
// give vis the properties of config
|
||||
_.assign(vis, opts.config);
|
||||
|
||||
// also give it the on* interaction functions, if any
|
||||
_.assign(vis, opts.listeners);
|
||||
|
||||
vis._fillConfigsToMinimum();
|
||||
|
||||
// resolve the search source for this AdhocVis
|
||||
return Promise.cast((function () {
|
||||
if (opts.searchSource) {
|
||||
// did not create the source, so we won't destroy it either
|
||||
createdSource = false;
|
||||
return opts.searchSource;
|
||||
}
|
||||
|
||||
return courier.createSource('search');
|
||||
|
||||
}()))
|
||||
.then(function (searchSource) {
|
||||
// TODO: Should we abtract out the agg building stuff?
|
||||
searchSource.aggs(function () {
|
||||
// stores the config objects in queryDsl
|
||||
var dsl = {};
|
||||
// counter to ensure unique agg names
|
||||
var i = 0;
|
||||
// start at the root, but the current will move
|
||||
var current = dsl;
|
||||
|
||||
// continue to nest the aggs under each other
|
||||
// writes to the dsl object
|
||||
vis.getConfig().forEach(function (config) {
|
||||
current.aggs = {};
|
||||
var key = '_agg_' + (i++);
|
||||
|
||||
var aggDsl = {};
|
||||
aggDsl[config.agg] = config.aggParams;
|
||||
|
||||
current = current.aggs[key] = aggDsl;
|
||||
});
|
||||
|
||||
// set the dsl to the searchSource
|
||||
return dsl.aggs || {};
|
||||
});
|
||||
|
||||
vis.searchSource = searchSource;
|
||||
|
||||
return vis;
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Should this be abstracted somewhere? Its a copy/paste from _saved_vis.js
|
||||
vis._fillConfigsToMinimum = function () {
|
||||
// satify the min count for each category
|
||||
configCats.fetchOrder.forEach(function (category) {
|
||||
var myCat = vis[category.name];
|
||||
|
||||
if (myCat.configs.length < myCat.min) {
|
||||
_.times(myCat.min - myCat.configs.length, function () {
|
||||
vis.addConfig(category.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
vis.destroy = function () {
|
||||
if (createdSource) {
|
||||
this.searchSource.cancelPending();
|
||||
} else {
|
||||
//remove our aggregations from the serarch source
|
||||
this.searchSource.set('aggs', null);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a list of config objects, which are ready to be turned into aggregations,
|
||||
* in the order which they should be executed.
|
||||
*
|
||||
* @return {Array} - The list of config objects
|
||||
*/
|
||||
vis.getConfig = Private(require('apps/visualize/saved_visualizations/_read_config'));
|
||||
|
||||
/**
|
||||
* Transform an ES Response into data for this visualization
|
||||
* @param {object} resp The elasticsearch response
|
||||
* @return {array} An array of flattened response rows
|
||||
*/
|
||||
vis.buildChartDataFromResponse = Private(require('apps/visualize/saved_visualizations/_build_chart_data'));
|
||||
|
||||
}
|
||||
|
||||
return AdhocVis;
|
||||
});
|
||||
});
|
|
@ -1,103 +0,0 @@
|
|||
define(function (require) {
|
||||
return function AggsService(Private) {
|
||||
require('lodash');
|
||||
var _ = require('lodash');
|
||||
|
||||
var aggs = {};
|
||||
|
||||
aggs.metricAggs = [
|
||||
{
|
||||
name: 'count',
|
||||
display: 'Count',
|
||||
types: ['number'],
|
||||
makeLabel: function (params) {
|
||||
return 'Count of documents';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'avg',
|
||||
display: 'Average',
|
||||
types: ['number'],
|
||||
makeLabel: function (params) {
|
||||
return 'Average ' + params.field;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'sum',
|
||||
display: 'Sum',
|
||||
types: ['number'],
|
||||
makeLabel: function (params) {
|
||||
return 'Sum of ' + params.field;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'min',
|
||||
display: 'Min',
|
||||
types: ['number'],
|
||||
makeLabel: function (params) {
|
||||
return 'Min ' + params.field;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'max',
|
||||
display: 'Max',
|
||||
types: ['number'],
|
||||
makeLabel: function (params) {
|
||||
return 'Max ' + params.field;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cardinality',
|
||||
display: 'Unique count',
|
||||
types: ['*'],
|
||||
makeLabel: function (params) {
|
||||
return 'Unique count of ' + params.field;
|
||||
}
|
||||
},
|
||||
];
|
||||
aggs.metricAggsByName = _.indexBy(aggs.metricAggs, 'name');
|
||||
|
||||
aggs.bucketAggs = Private(require('apps/visualize/saved_visualizations/bucket_aggs/_index'));
|
||||
aggs.bucketAggsByName = _.indexBy(aggs.bucketAggs, 'name');
|
||||
|
||||
aggs.byName = _.assign({}, aggs.bucketAggsByName, aggs.metricAggsByName);
|
||||
|
||||
aggs.byFieldType = {
|
||||
number: [
|
||||
aggs.bucketAggsByName.terms,
|
||||
aggs.bucketAggsByName.histogram,
|
||||
aggs.bucketAggsByName.range,
|
||||
// 'range'
|
||||
],
|
||||
date: [
|
||||
// 'date range',
|
||||
aggs.bucketAggsByName.date_histogram,
|
||||
aggs.bucketAggsByName.terms,
|
||||
],
|
||||
boolean: [
|
||||
aggs.bucketAggsByName.terms,
|
||||
// 'terms'
|
||||
],
|
||||
ip: [
|
||||
aggs.bucketAggsByName.terms,
|
||||
aggs.bucketAggsByName.ip_range,
|
||||
// 'ipv4 range'
|
||||
],
|
||||
geo_point: [
|
||||
aggs.bucketAggsByName.terms,
|
||||
// 'geo distance'
|
||||
],
|
||||
geo_shape: [
|
||||
aggs.bucketAggsByName.terms,
|
||||
// 'geohash grid'
|
||||
],
|
||||
string: [
|
||||
// 'significant terms',
|
||||
aggs.bucketAggsByName.terms,
|
||||
// 'range'
|
||||
]
|
||||
};
|
||||
|
||||
return aggs;
|
||||
};
|
||||
});
|
|
@ -1,53 +0,0 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var categories = [
|
||||
{
|
||||
name: 'segment',
|
||||
displayOrder: 2,
|
||||
fetchOrder: 1,
|
||||
min: 0,
|
||||
max: Infinity,
|
||||
configDefaults: {
|
||||
size: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'metric',
|
||||
displayOrder: 1,
|
||||
fetchOrder: 2,
|
||||
min: 0,
|
||||
max: 1,
|
||||
configDefaults: {
|
||||
agg: 'count'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
displayOrder: 3,
|
||||
fetchOrder: 3,
|
||||
min: 0,
|
||||
max: 1,
|
||||
configDefaults: {
|
||||
global: false,
|
||||
size: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'split',
|
||||
displayOrder: 4,
|
||||
fetchOrder: 4,
|
||||
min: 0,
|
||||
max: 2,
|
||||
configDefaults: {
|
||||
size: 5,
|
||||
row: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
categories.fetchOrder = _.sortBy(categories, 'fetchOrder');
|
||||
categories.displayOrder = _.sortBy(categories, 'displayOrder');
|
||||
categories.byName = _.indexBy(categories, 'name');
|
||||
|
||||
return categories;
|
||||
});
|
|
@ -1,111 +0,0 @@
|
|||
define(function (require) {
|
||||
return function ReadConfigFn(Private, $injector) {
|
||||
var _ = require('lodash');
|
||||
var configCategories = require('apps/visualize/saved_visualizations/_config_categories');
|
||||
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
|
||||
var courier = require('components/courier/courier');
|
||||
|
||||
return function readConfig() {
|
||||
var vis = this;
|
||||
|
||||
// these arrays represent the different sections used to create an aggregation, and when config objects are encountered
|
||||
// the are pushed into these array's based on their properties. Array's are used to make the logic and the final
|
||||
// combination simple. Many of these will be limited to a single value by the UI
|
||||
var positions = {
|
||||
// used to create rows/columns
|
||||
split: [],
|
||||
// global segments (eg. color, marked in the ui to be applied gloabally and the same values should be used across all charts)
|
||||
global: [],
|
||||
// primary segments (eg. x-axis)
|
||||
segment: [],
|
||||
// local segments (eg. color, marked in the ui that it should apply within each chart)
|
||||
local: [],
|
||||
// metric is the root "measurement" (eg. y-axis)
|
||||
metric: []
|
||||
};
|
||||
|
||||
function moveValidatedParam(input, output, paramDef, name) {
|
||||
if (!input[name]) {
|
||||
if (paramDef.default != null) input[name] = _.cloneDeep(paramDef.default);
|
||||
else return !paramDef.required;
|
||||
}
|
||||
|
||||
var val = input[name];
|
||||
var selectedOption = paramDef.options && _.find(paramDef.options, { val: val });
|
||||
if (!paramDef.custom && paramDef.options && !selectedOption) return false;
|
||||
|
||||
if (paramDef.write) {
|
||||
var selection = selectedOption;
|
||||
// either the value is custom or there just aren't any options defined
|
||||
if (!selectedOption && val != null) selection = { val: val };
|
||||
|
||||
// provide a hook to apply custom logic when writing this config value
|
||||
paramDef.write(selection, output);
|
||||
} else {
|
||||
// copy over the param
|
||||
output.aggParams[name] = val;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function makeCategoryValidator(category) {
|
||||
return function categoryValidator(config) {
|
||||
// filter out plain unusable configs
|
||||
if (!config || !config.agg || !config.field) return;
|
||||
|
||||
// get the agg used by this config
|
||||
var agg = aggs.byName[config.agg];
|
||||
if (!agg || agg.name === 'count') return;
|
||||
|
||||
// copy parts of the config to the validated "output" object
|
||||
var output = {
|
||||
agg: config.agg,
|
||||
aggParams: {},
|
||||
categoryName: category.name
|
||||
};
|
||||
|
||||
if (agg.name !== 'filters') output.aggParams.field = config.field;
|
||||
|
||||
// copy over other properties based on the category
|
||||
switch (category.name) {
|
||||
case 'split':
|
||||
output.row = !!config.row;
|
||||
break;
|
||||
case 'group':
|
||||
output.global = !!config.global;
|
||||
break;
|
||||
}
|
||||
|
||||
// this function will move valus from config.* to output.aggParams.* when they are
|
||||
// needed for that aggregation, and return true or false based on if all requirements
|
||||
// are meet
|
||||
var moveToAggParams = _.partial(moveValidatedParam, config, output);
|
||||
|
||||
// ensure that all of the declared params for the agg are declared on the config
|
||||
if (_.every(agg.params, moveToAggParams)) return output;
|
||||
};
|
||||
}
|
||||
|
||||
// collect all of the configs from each category,
|
||||
// validate them, filter the invalid ones, and put them into positions
|
||||
configCategories.fetchOrder.forEach(function (category) {
|
||||
var configs = vis[category.name].configs;
|
||||
|
||||
configs = configs
|
||||
.map(makeCategoryValidator(category))
|
||||
.filter(Boolean);
|
||||
|
||||
if (category.name === 'group') {
|
||||
positions.global = _.where(configs, { global: true });
|
||||
positions.local = _.where(configs, { global: false });
|
||||
} else {
|
||||
positions[category.name] = configs;
|
||||
}
|
||||
});
|
||||
|
||||
// join all of the different positions into a single array
|
||||
return positions.global.concat(positions.split, positions.segment, positions.local, positions.metric);
|
||||
};
|
||||
};
|
||||
});
|
|
@ -1,36 +1,28 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var inherits = require('lodash').inherits;
|
||||
require('modules')
|
||||
.get('app/visualize')
|
||||
.factory('SavedVis', function (config, $injector, courier, Promise, savedSearches, Private, Notifier) {
|
||||
var _ = require('lodash');
|
||||
var Vis = Private(require('components/vis/vis'));
|
||||
|
||||
var configCats = require('apps/visualize/saved_visualizations/_config_categories');
|
||||
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
|
||||
|
||||
var module = require('modules').get('app/visualize');
|
||||
|
||||
module.factory('SavedVis', function (config, $injector, courier, indexPatterns, Promise, savedSearches, Private) {
|
||||
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
|
||||
var notify = new Notifier({
|
||||
location: 'SavedVis'
|
||||
});
|
||||
|
||||
_(SavedVis).inherits(courier.SavedObject);
|
||||
function SavedVis(opts) {
|
||||
var vis = this;
|
||||
var self = this;
|
||||
opts = opts || {};
|
||||
if (typeof opts !== 'object') opts = { id: opts };
|
||||
|
||||
if (typeof opts !== 'object') {
|
||||
opts = {
|
||||
id: opts
|
||||
};
|
||||
}
|
||||
|
||||
var defaultParent = opts.parentSearchSource;
|
||||
|
||||
courier.SavedObject.call(vis, {
|
||||
SavedVis.Super.call(self, {
|
||||
type: 'visualization',
|
||||
|
||||
id: opts.id,
|
||||
|
||||
mapping: {
|
||||
title: 'string',
|
||||
typeName: 'string',
|
||||
stateJSON: 'string',
|
||||
visState: 'json',
|
||||
description: 'string',
|
||||
savedSearchId: 'string',
|
||||
indexPattern: 'string'
|
||||
|
@ -38,8 +30,12 @@ define(function (require) {
|
|||
|
||||
defaults: {
|
||||
title: '',
|
||||
typeName: opts.type || 'histogram',
|
||||
stateJSON: null,
|
||||
visState: (function () {
|
||||
if (!opts.type) return null;
|
||||
var def = {};
|
||||
def.type = opts.type;
|
||||
return def;
|
||||
}()),
|
||||
description: '',
|
||||
savedSearchId: opts.savedSearchId,
|
||||
indexPattern: opts.indexPattern
|
||||
|
@ -47,173 +43,68 @@ define(function (require) {
|
|||
|
||||
searchSource: true,
|
||||
|
||||
afterESResp: function setVisState() {
|
||||
if (!vis.typeDef || vis.typeName !== vis.typeDef.name) {
|
||||
// refresh the typeDef
|
||||
vis.typeDef = typeDefs.byName[vis.typeName];
|
||||
// refresh the defaults for all config categories
|
||||
configCats.forEach(function (category) {
|
||||
vis._initConfigCategory(category, vis[category.name]);
|
||||
});
|
||||
}
|
||||
afterESResp: this._afterEsResp
|
||||
});
|
||||
}
|
||||
|
||||
// get the saved state
|
||||
var state;
|
||||
if (vis.stateJSON) try { state = JSON.parse(vis.stateJSON); } catch (e) {}
|
||||
SavedVis.prototype._afterEsResp = function () {
|
||||
var self = this;
|
||||
var relatedSearch = self.savedSearchId;
|
||||
var relatedPattern = !relatedSearch && self.indexPattern;
|
||||
|
||||
// set the state on the vis
|
||||
if (state) vis.setState(state);
|
||||
var promisedParent = (function () {
|
||||
if (relatedSearch) {
|
||||
// returns a promise
|
||||
return savedSearches.get(self.savedSearchId);
|
||||
}
|
||||
|
||||
var relatedSearch = vis.savedSearchId;
|
||||
var relatedPattern = !relatedSearch && vis.indexPattern;
|
||||
var fakeSavedSearch = {
|
||||
searchSource: courier.createSource('search')
|
||||
};
|
||||
|
||||
var promisedParent = (function () {
|
||||
if (relatedSearch) {
|
||||
// returns a promise
|
||||
return savedSearches.get(vis.savedSearchId);
|
||||
}
|
||||
|
||||
var fakeSavedSearch = {
|
||||
searchSource: courier.createSource('search')
|
||||
};
|
||||
|
||||
if (relatedPattern) {
|
||||
return indexPatterns.get(relatedPattern)
|
||||
.then(function (indexPattern) {
|
||||
fakeSavedSearch.searchSource.index(indexPattern);
|
||||
return fakeSavedSearch;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(fakeSavedSearch);
|
||||
|
||||
}());
|
||||
|
||||
return promisedParent
|
||||
.then(function (parent) {
|
||||
vis.savedSearch = parent;
|
||||
|
||||
vis.searchSource
|
||||
.inherits(parent.searchSource)
|
||||
.size(0)
|
||||
// reads the vis' config and write the agg to the searchSource
|
||||
.aggs(function () {
|
||||
// stores the config objects in queryDsl
|
||||
var dsl = {};
|
||||
// counter to ensure unique agg names
|
||||
var i = 0;
|
||||
// start at the root, but the current will move
|
||||
var current = dsl;
|
||||
|
||||
// continue to nest the aggs under each other
|
||||
// writes to the dsl object
|
||||
vis.getConfig().forEach(function (config) {
|
||||
current.aggs = {};
|
||||
var key = '_agg_' + (i++);
|
||||
|
||||
var aggDsl = {};
|
||||
aggDsl[config.agg] = config.aggParams;
|
||||
|
||||
current = current.aggs[key] = aggDsl;
|
||||
});
|
||||
|
||||
// set the dsl to the searchSource
|
||||
return dsl.aggs || {};
|
||||
});
|
||||
|
||||
vis._fillConfigsToMinimum();
|
||||
|
||||
_.assign(vis, vis.typeDef.listeners);
|
||||
|
||||
return vis;
|
||||
if (relatedPattern) {
|
||||
return courier.indexPatterns.get(relatedPattern)
|
||||
.then(function (indexPattern) {
|
||||
fakeSavedSearch.searchSource.index(indexPattern);
|
||||
return fakeSavedSearch;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(fakeSavedSearch);
|
||||
}());
|
||||
|
||||
return promisedParent
|
||||
.then(function (parent) {
|
||||
self.savedSearch = parent;
|
||||
|
||||
self.searchSource
|
||||
.inherits(parent.searchSource)
|
||||
.size(0);
|
||||
|
||||
if (!self.vis) {
|
||||
self.vis = self._createVis();
|
||||
} else {
|
||||
self.vis.indexPattern = self.searchSource.get('index');
|
||||
self.vis.setState(self.visState);
|
||||
}
|
||||
|
||||
self.searchSource.aggs(function () {
|
||||
return self.vis.aggs.toDSL();
|
||||
});
|
||||
|
||||
return self;
|
||||
});
|
||||
};
|
||||
|
||||
vis.addConfig = function (categoryName) {
|
||||
var category = configCats.byName[categoryName];
|
||||
var config = _.defaults({}, category.configDefaults);
|
||||
SavedVis.prototype._createVis = function () {
|
||||
var indexPattern = this.searchSource.get('index');
|
||||
|
||||
vis[category.name].configs.push(config);
|
||||
if (this.stateJSON) {
|
||||
this.visState = Vis.convertOldState(this.typeName, JSON.parse(this.stateJSON));
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
vis.removeConfig = function (config) {
|
||||
if (!config) return;
|
||||
configCats.forEach(function (category) {
|
||||
_.pull(vis[category.name].configs, config);
|
||||
});
|
||||
};
|
||||
|
||||
vis._fillConfigsToMinimum = function () {
|
||||
|
||||
// satify the min count for each category
|
||||
configCats.fetchOrder.forEach(function (category) {
|
||||
var myCat = vis[category.name];
|
||||
|
||||
if (myCat.configs.length < myCat.min) {
|
||||
_.times(myCat.min - myCat.configs.length, function () {
|
||||
vis.addConfig(category.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// init the config category, optionally pass in an existing category to refresh
|
||||
// it's defaults based on the
|
||||
vis._initConfigCategory = function (category, cat) {
|
||||
cat = cat || {};
|
||||
|
||||
if (vis.typeDef) _.assign(cat, category, vis.typeDef.config[category.name]);
|
||||
cat.configDefaults = _.clone(category.configDefaults),
|
||||
cat.configs = cat.config || [];
|
||||
|
||||
vis[category.name] = cat;
|
||||
|
||||
return cat;
|
||||
};
|
||||
|
||||
vis.setState = function (state) {
|
||||
configCats.forEach(function (category) {
|
||||
var categoryStates = state[category.name] || [];
|
||||
vis[category.name].configs.splice(0);
|
||||
categoryStates.forEach(function (configState) {
|
||||
var config = vis.addConfig(category.name);
|
||||
_.assign(config, configState);
|
||||
});
|
||||
});
|
||||
|
||||
vis._fillConfigsToMinimum();
|
||||
};
|
||||
|
||||
vis.getState = function () {
|
||||
return _.transform(configCats, function (state, category) {
|
||||
var configs = state[category.name] = [];
|
||||
|
||||
[].push.apply(configs, vis[category.name].configs.map(function (config) {
|
||||
return _.pick(config, function (val, key) {
|
||||
return key.substring(0, 2) !== '$$';
|
||||
});
|
||||
}));
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a list of config objects, which are ready to be turned into aggregations,
|
||||
* in the order which they should be executed.
|
||||
*
|
||||
* @return {Array} - The list of config objects
|
||||
*/
|
||||
vis.getConfig = Private(require('apps/visualize/saved_visualizations/_read_config'));
|
||||
/**
|
||||
* Transform an ES Response into data for this visualization
|
||||
* @param {object} resp The elasticsearch response
|
||||
* @return {array} An array of flattened response rows
|
||||
*/
|
||||
vis.buildChartDataFromResponse = Private(require('apps/visualize/saved_visualizations/_build_chart_data'));
|
||||
}
|
||||
inherits(SavedVis, courier.SavedObject);
|
||||
return new Vis(indexPattern, this.visState);
|
||||
};
|
||||
|
||||
return SavedVis;
|
||||
});
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
define(function (require) {
|
||||
var module = require('modules').get('apps/visualize');
|
||||
var _ = require('lodash');
|
||||
|
||||
var typeDefs = [
|
||||
{
|
||||
name: 'histogram',
|
||||
icon: 'icon-chart-bar',
|
||||
params: {
|
||||
shareYAxis: true,
|
||||
addTooltip: true,
|
||||
addLegend: true
|
||||
},
|
||||
listeners: {
|
||||
onClick: function (e) {
|
||||
// TODO: We need to be able to get ahold of angular services here
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
metric: {
|
||||
label: 'Y-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
segment: {
|
||||
label: 'X-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
group: {
|
||||
label: 'Color',
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
split: {
|
||||
label: 'Rows & Columns',
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'line',
|
||||
icon: 'icon-chart-bar',
|
||||
params: {
|
||||
shareYAxis: true,
|
||||
addTooltip: true,
|
||||
addLegend: true
|
||||
},
|
||||
listeners: {
|
||||
},
|
||||
config: {
|
||||
metric: {
|
||||
label: 'Y-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
segment: {
|
||||
// limitToOrderedAggs: true,
|
||||
label: 'X-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
group: {
|
||||
label: 'Color',
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
split: {
|
||||
label: 'Rows & Columns',
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'area',
|
||||
icon: 'icon-chart-bar',
|
||||
params: {
|
||||
shareYAxis: true,
|
||||
addTooltip: true,
|
||||
addLegend: true,
|
||||
isStacked: true
|
||||
},
|
||||
listeners: {
|
||||
onClick: function (e) {
|
||||
// TODO: We need to be able to get ahold of angular services here
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
metric: {
|
||||
label: 'Y-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
segment: {
|
||||
// limitToOrderedAggs: true,
|
||||
label: 'X-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
group: {
|
||||
label: 'Color',
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
split: {
|
||||
label: 'Rows & Columns',
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'pie',
|
||||
icon: 'icon-chart-bar',
|
||||
params: {
|
||||
addTooltip: true,
|
||||
addLegend: true
|
||||
},
|
||||
listeners: {
|
||||
},
|
||||
config: {
|
||||
metric: {
|
||||
label: 'Y-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
segment: {
|
||||
label: 'X-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
group: {
|
||||
label: 'Color',
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
split: {
|
||||
label: 'Rows & Columns',
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
typeDefs.byName = _.indexBy(typeDefs, 'name');
|
||||
|
||||
return typeDefs;
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
define(function (require) {
|
||||
return function AggsService(Private) {
|
||||
return [
|
||||
Private(require('apps/visualize/saved_visualizations/bucket_aggs/date_histogram')),
|
||||
Private(require('apps/visualize/saved_visualizations/bucket_aggs/histogram')),
|
||||
Private(require('apps/visualize/saved_visualizations/bucket_aggs/range')),
|
||||
Private(require('apps/visualize/saved_visualizations/bucket_aggs/ip_range')),
|
||||
Private(require('apps/visualize/saved_visualizations/bucket_aggs/terms')),
|
||||
Private(require('apps/visualize/saved_visualizations/bucket_aggs/filters')),
|
||||
Private(require('apps/visualize/saved_visualizations/bucket_aggs/significant_terms'))
|
||||
];
|
||||
};
|
||||
});
|
|
@ -1,119 +0,0 @@
|
|||
define(function (require) {
|
||||
return function DateHistogramAggDefinition(timefilter, config) {
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var interval = require('utils/interval');
|
||||
|
||||
// shorthand
|
||||
var ms = function (type) { return moment.duration(1, type).asMilliseconds(); };
|
||||
|
||||
var pickInterval = function (bounds, targetBuckets) {
|
||||
bounds || (bounds = timefilter.getBounds());
|
||||
return interval.calculate(bounds.min, bounds.max, targetBuckets);
|
||||
};
|
||||
|
||||
var agg = this;
|
||||
agg.name = 'date_histogram';
|
||||
agg.display = 'Date Histogram';
|
||||
agg.ordered = {date: true};
|
||||
|
||||
agg.makeLabel = function (params, fullConfig) {
|
||||
if (fullConfig.metricScaleText) return params.field + ' per ' + fullConfig.metricScaleText;
|
||||
|
||||
var aggInterval = _.find(agg.params.interval.options, { ms: interval.toMs(params.interval) });
|
||||
if (aggInterval) return aggInterval.display + ' ' + params.field;
|
||||
else return params.field + ' per ' + interval.describe(params.interval);
|
||||
};
|
||||
|
||||
agg.params = {};
|
||||
agg.params.interval = {
|
||||
required: true,
|
||||
default: 'auto',
|
||||
custom: true,
|
||||
options: [
|
||||
{
|
||||
display: 'Auto',
|
||||
val: 'auto'
|
||||
},
|
||||
{
|
||||
display: 'Second',
|
||||
val: 'second',
|
||||
ms: ms('second')
|
||||
},
|
||||
{
|
||||
display: 'Minute',
|
||||
val: 'minute',
|
||||
ms: ms('minute')
|
||||
},
|
||||
{
|
||||
display: 'Hourly',
|
||||
val: 'hour',
|
||||
ms: ms('hour')
|
||||
},
|
||||
{
|
||||
display: 'Daily',
|
||||
val: 'day',
|
||||
ms: ms('day')
|
||||
},
|
||||
{
|
||||
display: 'Weekly',
|
||||
val: 'week',
|
||||
ms: ms('week')
|
||||
},
|
||||
{
|
||||
display: 'Monthly',
|
||||
val: 'month',
|
||||
ms: ms('month')
|
||||
},
|
||||
{
|
||||
display: 'Yearly',
|
||||
val: 'year',
|
||||
ms: ms('year')
|
||||
}
|
||||
],
|
||||
|
||||
write: function (selection, output) {
|
||||
var bounds = timefilter.getBounds();
|
||||
var auto;
|
||||
|
||||
if (selection.val === 'auto') {
|
||||
var bucketTarget = config.get('histogram:barTarget');
|
||||
auto = pickInterval(bounds, bucketTarget);
|
||||
output.aggParams.interval = auto.interval + 'ms';
|
||||
output.metricScaleText = auto.description;
|
||||
return;
|
||||
}
|
||||
|
||||
var ms = selection.ms || interval.toMs(selection.val);
|
||||
var buckets = Math.ceil((bounds.max - bounds.min) / ms);
|
||||
var maxBuckets = config.get('histogram:maxBars');
|
||||
if (buckets > maxBuckets) {
|
||||
// we should round these buckets out, and scale back the y values
|
||||
auto = pickInterval(bounds, maxBuckets);
|
||||
output.aggParams.interval = auto.interval + 'ms';
|
||||
output.metricScale = ms / auto.interval;
|
||||
output.metricScaleText = selection.val || auto.description;
|
||||
} else {
|
||||
output.aggParams.interval = selection.val;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
agg.params.format = {
|
||||
hide: true,
|
||||
custom: true
|
||||
};
|
||||
|
||||
agg.params.extended_bounds = {
|
||||
hide: true,
|
||||
default: {},
|
||||
write: function (selection, output) {
|
||||
var bounds = timefilter.getBounds();
|
||||
output.aggParams.extended_bounds = {
|
||||
min: bounds.min,
|
||||
max: bounds.max
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
define(function (require) {
|
||||
return function FiltersAggDefinition(timefilter, config) {
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var angular = require('angular');
|
||||
|
||||
var agg = this;
|
||||
agg.name = 'filters';
|
||||
agg.display = 'Filters';
|
||||
|
||||
agg.makeLabel = function (params) {
|
||||
return 'Filters';
|
||||
};
|
||||
|
||||
function getTickLabel(query) {
|
||||
if (query.query_string && query.query_string.query) return query.query_string.query;
|
||||
return JSON.stringify(query);
|
||||
}
|
||||
|
||||
agg.params = {};
|
||||
|
||||
agg.params.filters = {
|
||||
custom: true,
|
||||
default: {query_string: {query: '*'}},
|
||||
write: function (input, output) {
|
||||
output.aggParams = {
|
||||
filters: _.zipObject(_.map(input.val, function (filter, iterator) {
|
||||
// We need to check here
|
||||
return [
|
||||
getTickLabel(filter.input),
|
||||
{query: filter.input || {query_string: {query: '*'}}}
|
||||
];
|
||||
}))
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
define(function (require) {
|
||||
return function HistogramAggDefinition(timefilter, config) {
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
|
||||
var agg = this;
|
||||
agg.name = 'histogram';
|
||||
agg.display = 'Histogram';
|
||||
agg.ordered = {};
|
||||
|
||||
agg.makeLabel = function (params) {
|
||||
return params.field;
|
||||
};
|
||||
|
||||
agg.params = {};
|
||||
agg.params.interval = {
|
||||
required: true,
|
||||
write: function (input, output) {
|
||||
output.aggParams.interval = parseInt(input.val, 10);
|
||||
}
|
||||
};
|
||||
|
||||
agg.params.min_doc_count = {
|
||||
custom: true,
|
||||
default: false,
|
||||
write: function (input, output) {
|
||||
if (input.val) output.aggParams.min_doc_count = 0;
|
||||
else delete output.aggParams.min_doc_count;
|
||||
}
|
||||
};
|
||||
|
||||
agg.params.extended_bounds = {
|
||||
default: {},
|
||||
write: function (input, output) {
|
||||
output.aggParams.extended_bounds = {
|
||||
min: input.val.min,
|
||||
max: input.val.max
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
define(function (require) {
|
||||
require('directives/validate_ip');
|
||||
|
||||
return function RangeAggDefinition(timefilter, config) {
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var angular = require('angular');
|
||||
|
||||
var agg = this;
|
||||
agg.name = 'ip_range';
|
||||
agg.display = 'IP Range';
|
||||
//agg.ordered = {};
|
||||
|
||||
agg.makeLabel = function (params) {
|
||||
return params.field;
|
||||
};
|
||||
|
||||
agg.params = {};
|
||||
|
||||
agg.params.ranges = {
|
||||
custom: true,
|
||||
default: [{from: '0.0.0.0', to: '255.255.255.255'}],
|
||||
write: function (input, output) {
|
||||
output.aggParams.ranges = input.val;
|
||||
output.aggParams.keyed = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
});
|
|
@ -1,29 +0,0 @@
|
|||
define(function (require) {
|
||||
return function RangeAggDefinition(timefilter, config) {
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var angular = require('angular');
|
||||
|
||||
var agg = this;
|
||||
agg.name = 'range';
|
||||
agg.display = 'Range';
|
||||
//agg.ordered = {};
|
||||
|
||||
agg.makeLabel = function (params) {
|
||||
return params.field;
|
||||
};
|
||||
|
||||
agg.params = {};
|
||||
|
||||
agg.params.ranges = {
|
||||
custom: true,
|
||||
default: [{from: 0, to: 1000}, {from: 1000, to: 2000}],
|
||||
write: function (input, output) {
|
||||
output.aggParams.ranges = input.val;
|
||||
output.aggParams.keyed = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
define(function (require) {
|
||||
return function SignificantTermsAggDefinition() {
|
||||
var _ = require('lodash');
|
||||
|
||||
var agg = this;
|
||||
agg.name = 'significant_terms';
|
||||
agg.display = 'Significant Terms';
|
||||
|
||||
agg.makeLabel = function (params) {
|
||||
return 'Top ' + params.size + ' unusual terms in ' + params.field;
|
||||
};
|
||||
|
||||
agg.params = {
|
||||
size: {
|
||||
required: false,
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
define(function (require) {
|
||||
return function TermsAggDefinition() {
|
||||
var _ = require('lodash');
|
||||
|
||||
var agg = this;
|
||||
agg.name = 'terms';
|
||||
agg.display = 'Terms';
|
||||
|
||||
agg.makeLabel = function (params) {
|
||||
var order = _.find(agg.params.order.options, { val: params.order._count });
|
||||
return order.display + ' ' + params.size + ' ' + params.field;
|
||||
};
|
||||
|
||||
agg.params = {
|
||||
size: {
|
||||
required: false,
|
||||
},
|
||||
order: {
|
||||
required: true,
|
||||
options: [
|
||||
{ display: 'Top', val: 'desc' },
|
||||
{ display: 'Bottom', val: 'asc' }
|
||||
],
|
||||
default: 'desc',
|
||||
write: function (selection, output) {
|
||||
// TODO: We need more just _count here.
|
||||
output.aggParams.order = { _count: selection.val };
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
define(function (require) {
|
||||
return function RespConvertersService(Private) {
|
||||
var histogram = Private(require('apps/visualize/saved_visualizations/resp_converters/histogram'));
|
||||
return {
|
||||
histogram: histogram,
|
||||
line: histogram,
|
||||
area: histogram,
|
||||
pie: histogram
|
||||
};
|
||||
};
|
||||
});
|
|
@ -1,6 +1,5 @@
|
|||
define(function (require) {
|
||||
var app = require('modules').get('apps/visualize');
|
||||
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
|
||||
var app = require('modules').get('app/visualize');
|
||||
var _ = require('lodash');
|
||||
|
||||
require('apps/visualize/saved_visualizations/_saved_vis');
|
||||
|
@ -12,7 +11,11 @@ define(function (require) {
|
|||
title: 'visualizations'
|
||||
});
|
||||
|
||||
app.service('savedVisualizations', function (Promise, es, config, SavedVis) {
|
||||
app.service('savedVisualizations', function (Promise, es, config, SavedVis, Private, Notifier) {
|
||||
var visTypes = Private(require('components/vis_types/index'));
|
||||
var notify = new Notifier({
|
||||
location: 'saved visualization service'
|
||||
});
|
||||
|
||||
this.get = function (id) {
|
||||
return (new SavedVis(id)).init();
|
||||
|
@ -49,14 +52,26 @@ define(function (require) {
|
|||
.then(function (resp) {
|
||||
return {
|
||||
total: resp.hits.total,
|
||||
hits: resp.hits.hits.map(function (hit) {
|
||||
hits: _.transform(resp.hits.hits, function (hits, hit) {
|
||||
var source = hit._source;
|
||||
source.id = hit._id;
|
||||
source.url = self.urlFor(hit._id);
|
||||
source.typeDef = typeDefs.byName[source.typeName];
|
||||
source.icon = source.typeDef.icon;
|
||||
return source;
|
||||
})
|
||||
|
||||
var typeName = source.typeName;
|
||||
if (source.visState) {
|
||||
try { typeName = JSON.parse(source.visState).type; }
|
||||
catch (e) { /* missing typename handled below */ }
|
||||
}
|
||||
|
||||
if (!typeName) {
|
||||
notify.info('unable to detect type from visualization source', hit);
|
||||
return;
|
||||
}
|
||||
|
||||
source.type = visTypes.byName[typeName];
|
||||
source.icon = source.type.icon;
|
||||
hits.push(source);
|
||||
}, [])
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
define(function (require) {
|
||||
return function VisSpyReqRespStats() {
|
||||
var reqRespStatsHTML = require('text!apps/visualize/spy/_req_resp_stats.html');
|
||||
var linkReqRespStats = function ($scope, config) {
|
||||
$scope.$watchCollection('vis.searchSource.history', function (searchHistory) {
|
||||
if (!searchHistory) {
|
||||
$scope.history = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.history = searchHistory.map(function (entry) {
|
||||
if (!entry.complete || !entry.state) return;
|
||||
|
||||
var state = entry.state;
|
||||
var resp = entry.resp;
|
||||
var meta = [];
|
||||
|
||||
if (resp && resp.took != null) meta.push(['Query Duration', resp.took + 'ms']);
|
||||
if (entry && entry.ms != null) meta.push(['Request Duration', entry.ms + 'ms']);
|
||||
if (resp && resp.hits) meta.push(['Hits', resp.hits.total]);
|
||||
|
||||
if (state.index) meta.push(['Index', state.index]);
|
||||
if (state.type) meta.push(['Type', state.type]);
|
||||
if (state.id) meta.push(['Id', state.id]);
|
||||
|
||||
return {
|
||||
meta: meta,
|
||||
req: state.body,
|
||||
resp: entry.resp
|
||||
};
|
||||
}).filter(Boolean);
|
||||
});
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'request',
|
||||
display: 'Request',
|
||||
template: reqRespStatsHTML,
|
||||
link: linkReqRespStats
|
||||
},
|
||||
{
|
||||
name: 'response',
|
||||
display: 'Response',
|
||||
template: reqRespStatsHTML,
|
||||
link: linkReqRespStats
|
||||
},
|
||||
{
|
||||
name: 'stats',
|
||||
display: 'Statistics',
|
||||
template: reqRespStatsHTML,
|
||||
link: linkReqRespStats
|
||||
}
|
||||
];
|
||||
};
|
||||
});
|
|
@ -68,64 +68,31 @@
|
|||
.modal-footer:after {
|
||||
clear: both;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.vis-editor-content {
|
||||
display: flex;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: horizontal;
|
||||
-moz-box-orient: horizontal;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-pack: start;
|
||||
-moz-box-pack: start;
|
||||
-ms-flex-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.vis-editor-content .vis-sidebar {
|
||||
-webkit-box-flex: 0;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-flex: 0 0 300px;
|
||||
-ms-flex: 0 0 300px;
|
||||
flex: 0 0 300px;
|
||||
}
|
||||
.vis-editor-content .vis-canvas {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 1 100%;
|
||||
-ms-flex: 1 1 100%;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
.vis-editor-content vis-config-editor {
|
||||
display: block;
|
||||
}
|
||||
.vis-editor-content .sidebar-item-title:hover {
|
||||
color: inherit !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
.vis-editor-content .vis-config-details {
|
||||
border-top: 1px solid transparent;
|
||||
padding: 5px 10px;
|
||||
background-color: #ffffff;
|
||||
color: #444444;
|
||||
}
|
||||
.vis-editor-content .vis-config-details .config-controls {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.vis-editor-content .agg-config-interval td {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.vis-editor-content .agg-config-interval td:first-child {
|
||||
padding-left: 0px;
|
||||
}
|
||||
.vis-wizard h1 {
|
||||
margin-top: 45px;
|
||||
}
|
||||
vis-canvas {
|
||||
display: block;
|
||||
.vis-editor {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
.vis-editor > * {
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.vis-editor navbar .bitty-modal-container {
|
||||
position: relative;
|
||||
|
@ -147,6 +114,338 @@ vis-canvas {
|
|||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.vis-editor-content {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
.vis-editor-content > * {
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.vis-editor-content {
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: horizontal;
|
||||
-moz-box-orient: horizontal;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
.vis-editor-sidebar {
|
||||
-webkit-box-flex: 0;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
-ms-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
.vis-editor-sidebar > * {
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.vis-editor-sidebar {
|
||||
-webkit-flex-basis: 16.666666666666668%;
|
||||
flex-basis: 16.666666666666668%;
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
.vis-editor-sidebar .sidebar-container {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-right-color: #ecf0f1;
|
||||
}
|
||||
.vis-editor-sidebar .sidebar-item-title {
|
||||
background: #ecf0f1;
|
||||
}
|
||||
.vis-editor-sidebar .sidebar-item-title:hover {
|
||||
color: #444444 !important;
|
||||
background-color: #ecf0f1 !important;
|
||||
}
|
||||
.vis-editor nesting-indicator {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 0;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
-ms-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.vis-editor nesting-indicator > span {
|
||||
width: 3px;
|
||||
background-color: #31c471;
|
||||
-webkit-transition: width 0.3s ease-out;
|
||||
-moz-transition: width 0.3s ease-out;
|
||||
-o-transition: width 0.3s ease-out;
|
||||
transition: width 0.3s ease-out;
|
||||
}
|
||||
.vis-editor nesting-indicator > span.expand {
|
||||
width: 10px;
|
||||
}
|
||||
.vis-editor-agg {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
}
|
||||
.vis-editor-agg > * {
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.vis-editor-agg-wrapper {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
.vis-editor-agg-group {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
color: #444444;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.vis-editor-agg-group > * {
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.vis-editor-agg-header {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-moz-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
.vis-editor-agg-header-toggle {
|
||||
-webkit-box-flex: 0;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
-ms-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.vis-editor-agg-header-title {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-weight: bold;
|
||||
}
|
||||
.vis-editor-agg-header-description {
|
||||
font-weight: normal;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.vis-editor-agg-header-description.danger {
|
||||
color: #ffffff;
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
a.vis-editor-agg-header-description.danger:hover {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.vis-editor-agg-header-controls {
|
||||
-webkit-box-flex: 0;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
-ms-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.vis-editor-agg-editor {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.vis-editor-agg-editor-ranges td {
|
||||
padding: 0 5px 5px 0;
|
||||
}
|
||||
.vis-editor-agg-editor-ranges td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
.vis-editor-agg-form-row {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
.vis-editor-agg-form-row > * {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.vis-editor-agg-form-row > *:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
.vis-editor-agg-form-row > .btn {
|
||||
-webkit-align-self: center;
|
||||
-ms-flex-item-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
.vis-editor-agg-wide-btn {
|
||||
-webkit-border-radius: 0;
|
||||
-webkit-background-clip: padding-box;
|
||||
-moz-border-radius: 0;
|
||||
-moz-background-clip: padding;
|
||||
border-radius: 0;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.vis-editor-agg-add-form {
|
||||
margin: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
.vis-editor-agg-add-form > button {
|
||||
display: block;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
.vis-editor-canvas {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 0 975.3333333333334px;
|
||||
-ms-flex: 1 0 975.3333333333334px;
|
||||
flex: 1 0 975.3333333333334px;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.vis-editor-canvas {
|
||||
-webkit-flex-shrink: 1;
|
||||
flex-shrink: 1;
|
||||
-webkit-flex-basis: 100%;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
.vis-editor-canvas-title {
|
||||
text-align: center;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
.vis-editor-canvas visualize {
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.vis-editor-canvas visualize > * {
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.vis-editor-canvas .visualize-chart {
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-flex: 1 0 100%;
|
||||
-ms-flex: 1 0 100%;
|
||||
flex: 1 0 100%;
|
||||
position: relative;
|
||||
}
|
||||
form.vis-share div.form-control {
|
||||
height: inherit;
|
||||
}
|
||||
|
|
|
@ -1,103 +1,13 @@
|
|||
@import (reference) "../../../styles/_mixins.less";
|
||||
@import (reference) "../../../styles/_bootstrap.less";
|
||||
@import (reference) "../../../styles/theme/_theme.less";
|
||||
@import (reference) "../../../styles/_variables.less";
|
||||
@import (reference) "lesshat.less";
|
||||
|
||||
@media (min-width: @screen-md-min) {
|
||||
.vis-editor-content {
|
||||
display: flex;
|
||||
.flex-direction(row);
|
||||
.justify-content(flex-start);
|
||||
|
||||
.vis-sidebar {
|
||||
.flex(0, 0, 300px);
|
||||
}
|
||||
|
||||
.vis-canvas {
|
||||
.flex(1, 1, 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vis-editor-content {
|
||||
vis-config-editor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-item-title:hover {
|
||||
color: inherit !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.vis-config-details {
|
||||
border-top: 1px solid @well-border;
|
||||
padding: 5px 10px;
|
||||
background-color: @body-bg;
|
||||
color: @text-color;
|
||||
|
||||
.config-controls {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.agg-config-interval {
|
||||
td {
|
||||
padding-left: 10px;
|
||||
&:first-child {
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vis-wizard {
|
||||
h1 {
|
||||
margin-top: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
vis-canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vis-editor navbar {
|
||||
.bitty-modal-container {
|
||||
position: relative;
|
||||
|
||||
.bitty-modal {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
z-index: 10;
|
||||
background: rgba(70, 82, 93, 0.9);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding-top: 6px;
|
||||
.user-select(none);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
form.vis-share {
|
||||
div.form-control {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
// vis-search-editor {
|
||||
// display: block;
|
||||
// background: @sidebar-bg;
|
||||
// text-align: center;
|
||||
// min-height: 0;
|
||||
// border-bottom: 1px solid darken(@sidebar-bg, 10%);
|
||||
// .user-select(none);
|
||||
|
||||
// color: @sidebar-color;
|
||||
// a {
|
||||
// color: @sidebar-color;
|
||||
// }
|
||||
// }
|
||||
@import "../editor/editor.less";
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
<ul class="list-group list-group-menu">
|
||||
<a class="list-group-item list-group-menu-item"
|
||||
ng-repeat="type in visTypeDefs"
|
||||
ng-href="{{ typeUrl(type) }}">
|
||||
ng-repeat="type in visTypes"
|
||||
ng-href="{{ visTypeUrl(type) }}">
|
||||
<li>
|
||||
<i ng-class="type.icon"></i>{{type.name}}
|
||||
</li>
|
|
@ -1,26 +1,22 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
|
||||
|
||||
require('apps/visualize/saved_visualizations/saved_visualizations');
|
||||
require('directives/saved_object_finder');
|
||||
require('apps/discover/saved_searches/saved_searches');
|
||||
|
||||
var app = require('modules').get('apps/visualize', [
|
||||
'kibana/courier'
|
||||
]);
|
||||
|
||||
var routes = require('routes');
|
||||
|
||||
var templateStep = function (num, txt) {
|
||||
return '<div ng-controller="VisualizeWizardStep' + num + '" class="container vis-wizard">' + txt + '</div>';
|
||||
};
|
||||
|
||||
var module = require('modules').get('app/visualize', ['kibana/courier']);
|
||||
var routes = require('routes');
|
||||
|
||||
/********
|
||||
/** Wizard Step 1
|
||||
/********/
|
||||
routes.when('/visualize/step/1', {
|
||||
template: templateStep(1, require('text!apps/visualize/partials/wizard/step_1.html')),
|
||||
template: templateStep(1, require('text!apps/visualize/wizard/step_1.html')),
|
||||
resolve: {
|
||||
indexPatternIds: function (courier) {
|
||||
return courier.indexPatterns.getIds();
|
||||
|
@ -28,12 +24,12 @@ define(function (require) {
|
|||
}
|
||||
});
|
||||
|
||||
app.controller('VisualizeWizardStep1', function ($route, $scope, $location, timefilter) {
|
||||
module.controller('VisualizeWizardStep1', function ($route, $scope, $location, timefilter) {
|
||||
$scope.step2WithSearchUrl = function (hit) {
|
||||
return '#/visualize/step/2?savedSearchId=' + encodeURIComponent(hit.id);
|
||||
};
|
||||
|
||||
timefilter.enabled(false);
|
||||
timefilter.enabled = false;
|
||||
|
||||
$scope.indexPattern = {
|
||||
selection: null,
|
||||
|
@ -50,18 +46,18 @@ define(function (require) {
|
|||
/** Wizard Step 2
|
||||
/********/
|
||||
routes.when('/visualize/step/2', {
|
||||
template: templateStep(2, require('text!apps/visualize/partials/wizard/step_2.html'))
|
||||
template: templateStep(2, require('text!apps/visualize/wizard/step_2.html'))
|
||||
});
|
||||
|
||||
app.controller('VisualizeWizardStep2', function ($scope, $route, $location, timefilter) {
|
||||
module.controller('VisualizeWizardStep2', function ($scope, $route, $location, timefilter, Private) {
|
||||
var existing = _.pick($route.current.params, 'indexPattern', 'savedSearchId');
|
||||
|
||||
timefilter.enabled(false);
|
||||
timefilter.enabled = false;
|
||||
|
||||
$scope.visTypeDefs = typeDefs;
|
||||
$scope.typeUrl = function (type) {
|
||||
$scope.visTypes = Private(require('components/vis_types/index'));
|
||||
$scope.visTypeUrl = function (visType) {
|
||||
var query = _.defaults({
|
||||
type: type.name
|
||||
type: visType.name
|
||||
}, existing);
|
||||
|
||||
return '#/visualize/create?' + _.map(query, function (val, key) {
|
18
src/kibana/components/agg_types/README.md
Normal file
18
src/kibana/components/agg_types/README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
| Private | module id |
|
||||
| --- | --- |
|
||||
| `true` | `components/agg_config/index` |
|
||||
|
||||
```js
|
||||
var aggTypes = Private(require('components/agg_types/index'));
|
||||
```
|
||||
|
||||
Collection of `AggType` definition objects. See the [Vis component](../vis) for an overall explaination of how `AggTypes` are used.
|
||||
|
||||
### Included
|
||||
|
||||
- [`AggType`](_agg_type.js) class
|
||||
- `AggParam` classes
|
||||
- [`BaseAggParam`](param_types/base.js)
|
||||
- [`FieldAggParam`](param_types/field.js)
|
||||
- [`OptionedAggParam`](param_types/optioned.js)
|
||||
- [`AggParams`](_agg_params.js) class
|
46
src/kibana/components/agg_types/_agg_params.js
Normal file
46
src/kibana/components/agg_types/_agg_params.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
define(function (require) {
|
||||
return function AggParamsFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var Registry = require('utils/registry/registry');
|
||||
|
||||
var BaseAggParam = Private(require('components/agg_types/param_types/base'));
|
||||
var FieldAggParam = Private(require('components/agg_types/param_types/field'));
|
||||
var OptionedAggParam = Private(require('components/agg_types/param_types/optioned'));
|
||||
|
||||
_(AggParams).inherits(Registry);
|
||||
function AggParams(params) {
|
||||
AggParams.Super.call(this, {
|
||||
index: ['name'],
|
||||
group: ['required'],
|
||||
initialSet: params.map(function (param) {
|
||||
if (param.name === 'field') {
|
||||
return new FieldAggParam(param);
|
||||
}
|
||||
else if (param.options) {
|
||||
return new OptionedAggParam(param);
|
||||
}
|
||||
else {
|
||||
return new BaseAggParam(param);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
AggParams.prototype.write = function (aggConfig, locals) {
|
||||
var output = { params: {} };
|
||||
locals = locals || {};
|
||||
|
||||
this.forEach(function (param) {
|
||||
if (param.write) {
|
||||
param.write(aggConfig, output, locals);
|
||||
} else {
|
||||
output.params[param.name] = aggConfig.params[param.name];
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
return AggParams;
|
||||
};
|
||||
});
|
29
src/kibana/components/agg_types/_agg_type.js
Normal file
29
src/kibana/components/agg_types/_agg_type.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
define(function (require) {
|
||||
return function AggTypeFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var AggParams = Private(require('components/agg_types/_agg_params'));
|
||||
|
||||
function AggType(config) {
|
||||
this.name = config.name;
|
||||
this.title = config.title;
|
||||
this.makeLabel = config.makeLabel || _.constant(this.name);
|
||||
this.ordered = config.ordered;
|
||||
|
||||
var params = this.params = config.params || [];
|
||||
|
||||
if (!(params instanceof AggParams)) {
|
||||
if (_.isPlainObject(params)) {
|
||||
// convert the names: details format into details[].name
|
||||
params = _.map(params, function (param, name) {
|
||||
param.name = name;
|
||||
return param;
|
||||
});
|
||||
}
|
||||
|
||||
params = this.params = new AggParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
return AggType;
|
||||
};
|
||||
});
|
50
src/kibana/components/agg_types/buckets/_interval_options.js
Normal file
50
src/kibana/components/agg_types/buckets/_interval_options.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
define(function (require) {
|
||||
return function IntervalOptionsService(Private) {
|
||||
var moment = require('moment');
|
||||
|
||||
// shorthand
|
||||
var ms = function (type) { return moment.duration(1, type).asMilliseconds(); };
|
||||
|
||||
return [
|
||||
{
|
||||
display: 'Auto',
|
||||
val: 'auto'
|
||||
},
|
||||
{
|
||||
display: 'Second',
|
||||
val: 'second',
|
||||
ms: ms('second')
|
||||
},
|
||||
{
|
||||
display: 'Minute',
|
||||
val: 'minute',
|
||||
ms: ms('minute')
|
||||
},
|
||||
{
|
||||
display: 'Hourly',
|
||||
val: 'hour',
|
||||
ms: ms('hour')
|
||||
},
|
||||
{
|
||||
display: 'Daily',
|
||||
val: 'day',
|
||||
ms: ms('day')
|
||||
},
|
||||
{
|
||||
display: 'Weekly',
|
||||
val: 'week',
|
||||
ms: ms('week')
|
||||
},
|
||||
{
|
||||
display: 'Monthly',
|
||||
val: 'month',
|
||||
ms: ms('month')
|
||||
},
|
||||
{
|
||||
display: 'Yearly',
|
||||
val: 'year',
|
||||
ms: ms('year')
|
||||
}
|
||||
];
|
||||
};
|
||||
});
|
117
src/kibana/components/agg_types/buckets/date_histogram.js
Normal file
117
src/kibana/components/agg_types/buckets/date_histogram.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
define(function (require) {
|
||||
return function DateHistogramAggType(timefilter, config, Private) {
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var interval = require('utils/interval');
|
||||
var AggType = Private(require('components/agg_types/_agg_type'));
|
||||
|
||||
require('filters/field_type');
|
||||
|
||||
var pickInterval = function (bounds, targetBuckets) {
|
||||
bounds || (bounds = timefilter.getBounds());
|
||||
return interval.calculate(bounds.min, bounds.max, targetBuckets);
|
||||
};
|
||||
|
||||
return new AggType({
|
||||
name: 'date_histogram',
|
||||
title: 'Date Histogram',
|
||||
ordered: {
|
||||
date: true
|
||||
},
|
||||
makeLabel: function (aggConfig) {
|
||||
var output = this.params.write(aggConfig);
|
||||
var params = output.params;
|
||||
|
||||
if (output.metricScaleText) return params.field + ' per ' + output.metricScaleText;
|
||||
|
||||
var aggInterval = _.find(this.params.byName.interval.options, {
|
||||
ms: interval.toMs(params.interval)
|
||||
});
|
||||
|
||||
if (aggInterval) return aggInterval.display + ' ' + params.field;
|
||||
else return params.field + ' per ' + interval.describe(params.interval);
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
filterFieldTypes: 'date'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'interval',
|
||||
default: 'auto',
|
||||
options: Private(require('components/agg_types/buckets/_interval_options')),
|
||||
editor: require('text!components/agg_types/controls/interval.html'),
|
||||
write: function (aggConfig, output, locals) {
|
||||
var bounds = timefilter.getBounds();
|
||||
var auto;
|
||||
|
||||
var selection = aggConfig.params.interval;
|
||||
if (!_.isObject(selection)) {
|
||||
// custom selection
|
||||
selection = {
|
||||
display: selection,
|
||||
val: selection
|
||||
};
|
||||
}
|
||||
|
||||
if (selection.val === 'auto') {
|
||||
if (locals.renderBot) {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
var bucketTarget = config.get('histogram:barTarget');
|
||||
auto = pickInterval(bounds, bucketTarget);
|
||||
output.params.interval = auto.interval + 'ms';
|
||||
output.metricScaleText = auto.description;
|
||||
return;
|
||||
}
|
||||
|
||||
var ms = selection.ms || interval.toMs(selection.val);
|
||||
var buckets = Math.ceil((bounds.max - bounds.min) / ms);
|
||||
var maxBuckets = config.get('histogram:maxBars');
|
||||
if (buckets > maxBuckets) {
|
||||
// we should round these buckets out, and scale back the y values
|
||||
auto = pickInterval(bounds, maxBuckets);
|
||||
output.params.interval = auto.interval + 'ms';
|
||||
output.metricScale = ms / auto.interval;
|
||||
output.metricScaleText = selection.val || auto.description;
|
||||
} else {
|
||||
output.params.interval = selection.val;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'format'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'min_doc_count',
|
||||
default: 1
|
||||
},
|
||||
|
||||
{
|
||||
name: 'extended_bounds',
|
||||
default: {},
|
||||
write: function (aggConfig, output) {
|
||||
var val = aggConfig.params.extended_bounds;
|
||||
|
||||
if (val.min != null || val.max != null) {
|
||||
output.params.extended_bounds = {
|
||||
min: val.min,
|
||||
max: val.max
|
||||
};
|
||||
} else {
|
||||
var tfBounds = timefilter.getBounds();
|
||||
output.params.extended_bounds = {
|
||||
min: tfBounds.min,
|
||||
max: tfBounds.max,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
36
src/kibana/components/agg_types/buckets/filters.js
Normal file
36
src/kibana/components/agg_types/buckets/filters.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
define(function (require) {
|
||||
return function FiltersAggDefinition(Private) {
|
||||
var _ = require('lodash');
|
||||
var AggType = Private(require('components/agg_types/_agg_type'));
|
||||
|
||||
function getTickLabel(query) {
|
||||
if (query.query_string && query.query_string.query) {
|
||||
return query.query_string.query;
|
||||
}
|
||||
|
||||
return JSON.stringify(query);
|
||||
}
|
||||
|
||||
return new AggType({
|
||||
name: 'filters',
|
||||
title: 'Filters',
|
||||
params: [
|
||||
{
|
||||
name: 'filters',
|
||||
editor: require('text!components/agg_types/controls/filters.html'),
|
||||
default: [ {} ],
|
||||
write: function (aggConfig, output) {
|
||||
output.aggParams = {
|
||||
filters: _.transform(aggConfig.params.filters, function (filters, filter, iterator) {
|
||||
// We need to check here
|
||||
filters[getTickLabel(filter.input)] = {
|
||||
query: filter.input || { query_string: {query: '*'} }
|
||||
};
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
68
src/kibana/components/agg_types/buckets/histogram.js
Normal file
68
src/kibana/components/agg_types/buckets/histogram.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
define(function (require) {
|
||||
return function HistogramAggDefinition(Private) {
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var AggType = Private(require('components/agg_types/_agg_type'));
|
||||
|
||||
return new AggType({
|
||||
name: 'histogram',
|
||||
title: 'Histogram',
|
||||
ordered: {},
|
||||
makeLabel: function (aggConfig) {
|
||||
return aggConfig.params.field.name;
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
filterFieldTypes: 'number'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'interval',
|
||||
editor: require('text!components/agg_types/controls/interval.html'),
|
||||
write: function (aggConfig, output) {
|
||||
output.params.interval = parseInt(aggConfig.params.interval, 10);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'min_doc_count',
|
||||
default: false,
|
||||
editor: require('text!components/agg_types/controls/min_doc_count.html'),
|
||||
write: function (aggConfig, output) {
|
||||
if (aggConfig.params.min_doc_count) {
|
||||
output.params.min_doc_count = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'extended_bounds',
|
||||
default: {},
|
||||
editor: require('text!components/agg_types/controls/extended_bounds.html'),
|
||||
write: function (aggConfig, output) {
|
||||
var val = aggConfig.params.extended_bounds;
|
||||
|
||||
if (val.min != null || val.max != null) {
|
||||
output.params.extended_bounds = {
|
||||
min: val.min,
|
||||
max: val.max
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// called from the editor
|
||||
shouldShow: function (aggConfig) {
|
||||
var field = aggConfig.params.field;
|
||||
if (
|
||||
field
|
||||
&& (field.type === 'number' || field.type === 'date')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
34
src/kibana/components/agg_types/buckets/range.js
Normal file
34
src/kibana/components/agg_types/buckets/range.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
define(function (require) {
|
||||
return function RangeAggDefinition(Private) {
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var angular = require('angular');
|
||||
var AggType = Private(require('components/agg_types/_agg_type'));
|
||||
|
||||
return new AggType({
|
||||
name: 'range',
|
||||
title: 'Range',
|
||||
makeLabel: function (aggConfig) {
|
||||
return aggConfig.params.field.name + ' ranges';
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
filterFieldTypes: ['number', 'date', 'string']
|
||||
},
|
||||
{
|
||||
name: 'ranges',
|
||||
default: [
|
||||
{ from: 0, to: 1000 },
|
||||
{ from: 1000, to: 2000 }
|
||||
],
|
||||
editor: require('text!components/agg_types/controls/ranges.html'),
|
||||
write: function (aggConfig, output) {
|
||||
output.params.ranges = aggConfig.params.ranges;
|
||||
output.params.keyed = true;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
24
src/kibana/components/agg_types/buckets/significant_terms.js
Normal file
24
src/kibana/components/agg_types/buckets/significant_terms.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
define(function (require) {
|
||||
return function SignificantTermsAggDefinition(Private) {
|
||||
var _ = require('lodash');
|
||||
var AggType = Private(require('components/agg_types/_agg_type'));
|
||||
|
||||
return new AggType({
|
||||
name: 'significant_terms',
|
||||
title: 'Significant Terms',
|
||||
makeLabel: function (aggConfig) {
|
||||
return 'Top ' + aggConfig.params.size + ' unusual terms in ' + aggConfig.params.field.name;
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
filterFieldTypes: 'string'
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
editor: require('text!components/agg_types/controls/order_and_size.html'),
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
40
src/kibana/components/agg_types/buckets/terms.js
Normal file
40
src/kibana/components/agg_types/buckets/terms.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
define(function (require) {
|
||||
return function TermsAggDefinition(Private) {
|
||||
var _ = require('lodash');
|
||||
var AggType = Private(require('components/agg_types/_agg_type'));
|
||||
|
||||
return new AggType({
|
||||
name: 'terms',
|
||||
title: 'Terms',
|
||||
makeLabel: function (aggConfig) {
|
||||
var params = aggConfig.params;
|
||||
return params.order.display + ' ' + params.size + ' ' + params.field.name;
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field'
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
default: 5
|
||||
// editor: batched with order
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
options: [
|
||||
{ display: 'Top', val: 'desc' },
|
||||
{ display: 'Bottom', val: 'asc' }
|
||||
],
|
||||
editor: require('text!components/agg_types/controls/order_and_size.html'),
|
||||
default: 'desc',
|
||||
write: function (aggConfig, output) {
|
||||
// TODO: We need more than just _count here.
|
||||
output.params.order = {
|
||||
_count: aggConfig.params.order.val
|
||||
};
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
<div ng-if="aggParam.shouldShow(aggConfig)" class="vis-editor-agg-form-row">
|
||||
<div class="form-group">
|
||||
<label>Min <small>(optional)</small></label>
|
||||
<input
|
||||
ng-model="params.extended_bounds.min"
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="extended_bounds.min" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max <small>(optional)</small></label>
|
||||
<input
|
||||
ng-model="params.extended_bounds.max"
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="extended_bounds.max" />
|
||||
</div>
|
||||
</div>
|
17
src/kibana/components/agg_types/controls/field.html
Normal file
17
src/kibana/components/agg_types/controls/field.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="form-group">
|
||||
<label for="field">
|
||||
Field
|
||||
</label>
|
||||
<select
|
||||
class="form-control"
|
||||
name="field"
|
||||
required
|
||||
ng-model="params.field"
|
||||
ng-options="
|
||||
field as field.name group by field.type for field in aggConfig.vis.indexPattern.fields.raw
|
||||
| fieldType: aggParam.filterFieldTypes
|
||||
| filter: { indexed:true }
|
||||
| orderBy:['type','name']
|
||||
">
|
||||
</select>
|
||||
</div>
|
24
src/kibana/components/agg_types/controls/filters.html
Normal file
24
src/kibana/components/agg_types/controls/filters.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<div class="form-group">
|
||||
<div ng-repeat="filter in params.filters">
|
||||
<label>Query string {{$index + 1}}</label>
|
||||
<div class="form-group vis-editor-agg-form-row">
|
||||
<input query-input
|
||||
ng-model="filter.query"
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="filter{{$index}}">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
ng-click="params.filters.splice($index, 1)"
|
||||
class="btn btn-danger btn-xs">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ng-click="params.filters.push({})"
|
||||
class="sidebar-item-button primary">
|
||||
Add filter
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue