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