diff --git a/app/assets/javascripts/vue/settings.js b/app/assets/javascripts/vue/settings.js
new file mode 100644
index 0000000..6148e88
--- /dev/null
+++ b/app/assets/javascripts/vue/settings.js
@@ -0,0 +1,112 @@
+;(function(){
+ "use strict";
+
+ $(function(){
+ var el = document.querySelector("#vue-setting");
+ if(!el) return;
+
+ new Vue({
+ el: el,
+ data: {
+ loaded: false,
+ loading: false,
+ sections: {
+ sources: [],
+ matches: []
+ }
+ },
+ created: function() {
+ this.update();
+ },
+ components: {
+ section: {
+ template: "#vue-setting-section",
+ data: {
+ mode: "default",
+ processing: false,
+ editContent: null
+ },
+ created: function(){
+ this.initialState();
+ },
+ computed: {
+ endpoint: function(){
+ return "/api/settings/" + this.id;
+ }
+ },
+ methods: {
+ onCancel: function(ev) {
+ this.initialState();
+ },
+ onEdit: function(ev) {
+ this.mode = "edit";
+ },
+ onDelete: function(ev) {
+ if(!confirm("really?")) return;
+ this.destroy();
+ },
+ onSubmit: function(ev) {
+ this.processing = true;
+ var self = this;
+ $.ajax({
+ url: this.endpoint,
+ method: "POST",
+ data: {
+ _method: "PATCH",
+ id: this.id,
+ content: this.editContent
+ }
+ }).then(function(data){
+ // NOTE: child VM update doesn't effect to parent VM (at least Vue v0.10)
+ self.$data = data;
+ self.initialState();
+ }).always(function(){
+ self.processing = false;
+ });
+ },
+ initialState: function(){
+ this.mode = "default";
+ this.editContent = this.content;
+ },
+ destroy: function(){
+ var self = this;
+ $.ajax({
+ url: this.endpoint,
+ method: "POST",
+ data: {
+ _method: "DELETE",
+ id: this.id
+ }
+ }).then(function(){
+ self.$parent.update();
+ });
+ }
+ }
+ }
+ },
+ methods: {
+ update: function() {
+ this.loading = true;
+ var self = this;
+ $.getJSON("/api/settings", function(data){
+ var sources = [];
+ var matches = [];
+ data.forEach(function(v){
+ if(v.name === "source"){
+ sources.push(v);
+ }else{
+ matches.push(v);
+ }
+ });
+ self.sections.sources = sources;
+ self.sections.matches = matches;
+ self.loaded = true;
+ setTimeout(function(){
+ self.loading = false;
+ }, 500);
+ });
+ }
+ }
+ });
+ });
+})();
diff --git a/app/assets/stylesheets/common.css.scss b/app/assets/stylesheets/common.css.scss
index b60b47f..1cbe18f 100644
--- a/app/assets/stylesheets/common.css.scss
+++ b/app/assets/stylesheets/common.css.scss
@@ -150,3 +150,9 @@ label {
.nav > li > a.section {
color: #777;
}
+
+
+#vue-setting textarea {
+ min-height: 12em;
+ resize: both;
+}
diff --git a/app/controllers/api/settings_controller.rb b/app/controllers/api/settings_controller.rb
new file mode 100644
index 0000000..a781a02
--- /dev/null
+++ b/app/controllers/api/settings_controller.rb
@@ -0,0 +1,55 @@
+class Api::SettingsController < ApplicationController
+ before_action :login_required
+ before_action :find_fluentd
+ before_action :set_config
+ before_action :set_section, only: [:show, :update, :destroy]
+ helper_method :element_id
+ respond_to :json
+
+ def index
+ end
+
+ def update
+ coming = Fluent::Config::V1Parser.parse(params[:content], @fluentd.config_file)
+ current = @section
+ index = @config.elements.index current
+ unless index
+ render_404
+ return
+ end
+ @config.elements[index] = coming.elements.first
+ @config.write_to_file
+ redirect_to api_setting_path(id: element_id(coming.elements.first))
+ end
+
+ def destroy
+ unless @config.elements.index(@section)
+ render_404
+ return
+ end
+ @config.elements.delete @section
+ @config.write_to_file
+ head :no_content # 204
+ end
+
+ private
+
+ def set_config
+ @config = Fluentd::Setting::Config.new(@fluentd.config_file)
+ end
+
+ def set_section
+ @section = @config.elements.find do |elm|
+ element_id(elm) == params[:id]
+ end
+ end
+
+ def element_id(element)
+ index = @config.elements.index(element)
+ "#{"%06d" % index}#{Digest::MD5.hexdigest(element.to_s)}"
+ end
+
+ def render_404
+ render nothing: true, status: 404
+ end
+end
diff --git a/app/controllers/fluentd/settings_controller.rb b/app/controllers/fluentd/settings_controller.rb
index 4d5ad67..641f810 100644
--- a/app/controllers/fluentd/settings_controller.rb
+++ b/app/controllers/fluentd/settings_controller.rb
@@ -1,22 +1,33 @@
class Fluentd::SettingsController < ApplicationController
before_action :login_required
before_action :find_fluentd
+ before_action :set_config, only: [:show, :edit, :update]
def show
- @config = @fluentd.agent.config
end
def edit
- @config = @fluentd.agent.config
end
def update
+ Fluent::Config::V1Parser.parse(params[:config], @fluentd.config_file)
@fluentd.agent.config_write params[:config]
@fluentd.agent.restart if @fluentd.agent.running?
redirect_to daemon_setting_path(@fluentd)
+ rescue Fluent::ConfigParseError => e
+ @config = params[:config]
+ @error = e.message
+ render "edit"
end
def source_and_output
- @config = Fluentd::Setting::Config.new(@fluentd.config_file)
+ # TODO: error handling if config file has invalid syntax
+ # @config = Fluentd::Setting::Config.new(@fluentd.config_file)
+ end
+
+ private
+
+ def set_config
+ @config = @fluentd.agent.config
end
end
diff --git a/app/models/fluentd/setting/config.rb b/app/models/fluentd/setting/config.rb
index 6cc0b2c..45d0fed 100644
--- a/app/models/fluentd/setting/config.rb
+++ b/app/models/fluentd/setting/config.rb
@@ -3,29 +3,37 @@ require 'fluent/config'
class Fluentd
module Setting
class Config
- attr_reader :config, :file
+ attr_reader :fl_config, :file
+ delegate :elements, to: :fl_config
def initialize(config_file)
- config = Fluent::Config.parse(IO.read(config_file), config_file, nil, true)
- @config = config
+ @fl_config = Fluent::Config.parse(IO.read(config_file), config_file, nil, true)
@file = config_file
end
def empty?
- config.elements.length.zero?
+ elements.length.zero?
end
def sources
- config.elements.find_all do |elm|
+ elements.find_all do |elm|
elm.name == "source"
end
end
def matches
- config.elements.find_all do |elm|
+ elements.find_all do |elm|
elm.name == "match"
end
end
+
+ def write_to_file
+ File.open(file, "w"){|f| f.write formatted }
+ end
+
+ def formatted
+ fl_config.to_s.gsub(/<\/?ROOT>/, "").strip_heredoc.gsub(%r|^$|, "\\0\n")
+ end
end
end
end
diff --git a/app/views/api/settings/_element.json.jbuilder b/app/views/api/settings/_element.json.jbuilder
new file mode 100644
index 0000000..bc58a7d
--- /dev/null
+++ b/app/views/api/settings/_element.json.jbuilder
@@ -0,0 +1,6 @@
+json.id element_id(element)
+json.name element.name
+json.type element["type"]
+json.arg element.arg
+json.settings element
+json.content element.to_s
diff --git a/app/views/api/settings/index.json.jbuilder b/app/views/api/settings/index.json.jbuilder
new file mode 100644
index 0000000..2b565f8
--- /dev/null
+++ b/app/views/api/settings/index.json.jbuilder
@@ -0,0 +1,3 @@
+json.array! @config.elements do |elm|
+ json.partial! "api/settings/element", element: elm
+end
diff --git a/app/views/api/settings/show.json.jbuilder b/app/views/api/settings/show.json.jbuilder
new file mode 100644
index 0000000..a4e3486
--- /dev/null
+++ b/app/views/api/settings/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! "api/settings/element", element: @section
diff --git a/app/views/fluentd/settings/edit.html.haml b/app/views/fluentd/settings/edit.html.haml
index 382ee5a..43ac76a 100644
--- a/app/views/fluentd/settings/edit.html.haml
+++ b/app/views/fluentd/settings/edit.html.haml
@@ -1,5 +1,8 @@
- page_title t('.page_title', label: @fluentd.label)
+- if @error
+ %pre.alert.alert-danger= @error
+
= form_tag(daemon_setting_path(@fluentd), method: :patch) do
.form-group
= text_area_tag "config", @config, class: "form-control", rows: 40
diff --git a/app/views/fluentd/settings/source_and_output.html.haml b/app/views/fluentd/settings/source_and_output.html.haml
index 84f673f..28dba99 100644
--- a/app/views/fluentd/settings/source_and_output.html.haml
+++ b/app/views/fluentd/settings/source_and_output.html.haml
@@ -46,33 +46,21 @@
= icon('fa-file-text-o fa-lg')
= t("fluentd.common.setup_out_forward")
-.current-settings
- %h2= t('.current')
+= render "shared/vue/setting"
+
+#vue-setting.current-settings
+ %h2
+ = t('.current')
+ %span{"v-on" => "click: update", "v-if" => "!loading"}= icon('fa-refresh')
+ %span{"v-if" => "loading"}= icon('fa-spin fa-refresh')
.row
.col-xs-6.input
%h3= t('.in')
- - if @config.sources.empty?
+ %div{"v-if" => "loaded && sections.sources.length == 0"}
%p.empty= t('.setting_empty')
- - else
- - @config.sources.each_with_index do |elm, idx|
- .panel.panel-default
- .panel-heading{"data-toggle" => "collapse", "href" => "#source#{idx}", "title" => elm.inspect}
- = elm["type"]
- = icon('fa-caret-down')
- .panel-body.collapse{"id" => "source#{idx}"}
- %pre= elm.to_s
+ %div{"v-repeat" => "sections.sources", "v-component" => "section"}
.col-xs-6.output
%h3= t('.out')
- - if @config.matches.empty?
+ %div{"v-if" => "loaded && sections.matches.length == 0"}
%p.empty= t('.setting_empty')
- - else
- - @config.matches.each_with_index do |elm, idx|
- .panel.panel-default
- .panel-heading{"data-toggle" => "collapse", "href" => "#match#{idx}", "title" => elm.inspect}
- = elm["type"]
- (
- = elm.arg
- )
- = icon('fa-caret-down')
- .panel-body.collapse{"id" => "match#{idx}"}
- %pre= elm.to_s
+ %div{"v-repeat" => "sections.matches", "v-component" => "section"}
diff --git a/app/views/shared/vue/_setting.html.erb b/app/views/shared/vue/_setting.html.erb
new file mode 100644
index 0000000..513c27e
--- /dev/null
+++ b/app/views/shared/vue/_setting.html.erb
@@ -0,0 +1,23 @@
+
diff --git a/config/application.rb b/config/application.rb
index 1d09d9e..dfbc258 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -19,6 +19,7 @@ require "jquery-rails"
require "sucker_punch"
require "settingslogic"
require "kramdown-haml"
+require "jbuilder"
module FluentdUi
class Application < Rails::Application
diff --git a/config/locales/translation_en.yml b/config/locales/translation_en.yml
index 643a84f..9fad53e 100644
--- a/config/locales/translation_en.yml
+++ b/config/locales/translation_en.yml
@@ -31,8 +31,10 @@ en:
no_alert: Nothing
update_password: Update Password
detail: Detail
+ cancel: Cancel
create: Create
update: Update & Restart
+ save: Save
edit: Edit
destroy: Destroy
new: New
diff --git a/config/locales/translation_ja.yml b/config/locales/translation_ja.yml
index 873f90a..0e77d10 100644
--- a/config/locales/translation_ja.yml
+++ b/config/locales/translation_ja.yml
@@ -31,8 +31,10 @@ ja:
no_alert: 通知なし
update_password: パスワード更新
detail: 詳細
+ cancel: キャンセル
create: 作成
update: 更新
+ save: 保存
edit: 編集
destroy: 削除
new: 新規作成
diff --git a/config/routes.rb b/config/routes.rb
index 6bfc44d..c698a5d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -93,5 +93,7 @@ Rails.application.routes.draw do
get "file_preview"
post "regexp_preview"
post "grok_to_regexp"
+
+ resources :settings, only: [:index, :show, :update, :destroy], defaults: { format: "json" }
end
end
diff --git a/spec/features/fluentd/setting/source_and_output_spec.rb b/spec/features/fluentd/setting/source_and_output_spec.rb
index aba8121..089cded 100644
--- a/spec/features/fluentd/setting/source_and_output_spec.rb
+++ b/spec/features/fluentd/setting/source_and_output_spec.rb
@@ -89,4 +89,69 @@ describe "source_and_output", js: true do
end
end
end
+
+ describe "edit, update, delete" do
+ let(:config_contents) { <<-CONF.strip_heredoc }
+
+ type forward
+ port 24224
+
+ CONF
+ let(:new_config) { <<-CONF.strip_heredoc }
+
+ type http
+ port 8899
+
+ CONF
+
+ before do
+ all(".input .panel .panel-heading").first.click
+ end
+
+ it "click edit button transform textarea, then click cancel button to be reset" do
+ page.should_not have_css('.input textarea')
+ find(".btn", text: I18n.t('terms.edit')).click
+ page.should have_css('.input textarea')
+ find('.input textarea').value.should == config_contents
+ find('.input textarea').set "foo"
+ find(".btn", text: I18n.t('terms.cancel')).click
+ content = wait_until do
+ page.evaluate_script("document.querySelector('.input pre').textContent")
+ end
+ content.should == config_contents
+ daemon.agent.config.strip.should == config_contents.strip
+ end
+
+ it "click edit button transform textarea, then click update button to be stored" do
+ page.should_not have_css('.input textarea')
+ find(".btn", text: I18n.t('terms.edit')).click
+ page.should have_css('.input textarea')
+ find('.input textarea').value.should == config_contents
+ find('.input textarea').set new_config
+ find(".btn", text: I18n.t('terms.save')).click
+ content = wait_until do
+ page.evaluate_script("document.querySelector('.input pre').textContent")
+ end
+ content.should == new_config
+ daemon.agent.config.strip.should == new_config.strip
+ end
+
+ it "click delete button transform textarea" do
+ page.should have_css('.input .panel-body')
+ confirm_dialog(true) do
+ find(".btn", text: I18n.t('terms.destroy')).click
+ end
+ page.should_not have_css('.input .panel-body')
+ daemon.agent.config.strip.should == ""
+ end
+
+ it "click delete button then cancel it" do
+ page.should have_css('.input .panel-body')
+ confirm_dialog(false) do
+ find(".btn", text: I18n.t('terms.destroy')).click
+ end
+ page.should have_css('.input .panel-body')
+ daemon.agent.config.strip.should == config_contents.strip
+ end
+ end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 603e70f..532be0d 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -34,6 +34,7 @@ RSpec.configure do |config|
# Syntax sugar to use the FactoryGirl methods directly instead FactoryGirl.create ete.
config.include FactoryGirl::Syntax::Methods
config.include LoginMacro
+ config.include JavascriptMacro
# If true, the base class of anonymous controllers will be inferred
# automatically. This will be the default behavior in future versions of
diff --git a/spec/support/javascript_macro.rb b/spec/support/javascript_macro.rb
new file mode 100644
index 0000000..991ab86
--- /dev/null
+++ b/spec/support/javascript_macro.rb
@@ -0,0 +1,21 @@
+module JavascriptMacro
+ def wait_until(seconds = 5, &block)
+ timeout(seconds) do
+ loop do
+ begin
+ ret = block.call
+ break ret if ret
+ rescue Capybara::Poltergeist::JavascriptError
+ end
+ sleep 0.01
+ end
+ end
+ end
+
+ def confirm_dialog(ret, &block)
+ page.execute_script "__backup = window.confirm; window.confirm = function(){return #{ret};}"
+ block.call
+ ensure
+ page.execute_script "window.confirm = __backup;"
+ end
+end