Merge pull request #105 from fluent/setting_component

Setting component
This commit is contained in:
uu59 2014-11-26 17:31:39 +09:00
commit 71e44aa9d1
18 changed files with 342 additions and 32 deletions

View File

@ -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);
});
}
}
});
});
})();

View File

@ -150,3 +150,9 @@ label {
.nav > li > a.section {
color: #777;
}
#vue-setting textarea {
min-height: 12em;
resize: both;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
json.array! @config.elements do |elm|
json.partial! "api/settings/element", element: elm
end

View File

@ -0,0 +1 @@
json.partial! "api/settings/element", element: @section

View File

@ -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

View File

@ -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"}

View File

@ -0,0 +1,23 @@
<script type="text/template" id="vue-setting-section">
<div class='panel panel-default'>
<div class='panel-heading' data-toggle='collapse' href='#{{ id }}' title='{{ content }}'>
{{ type }}
<span v-if="name == 'match'">({{ arg }})</span>
<i class="fa fa-caret-down"></i>
</div>
<div class='panel-body collapse' id='{{ id }}'>
<pre v-if="mode != 'edit'">{{ content }}</pre>
<p v-if="mode == 'edit'">
<textarea class="form-control" v-model="editContent" v-attr="disabled: processing"></textarea>
</p>
<p class="pull-right">
<button v-if="mode == 'default'" class="btn btn-default" v-on="click: onEdit"><%= t('terms.edit') %></button>
<button v-if="mode == 'default'" class="btn btn-danger" v-on="click: onDelete"><%= t('terms.destroy') %></button>
<button v-if="mode != 'default'" class="btn btn-default" v-on="click: onCancel"><%= t('terms.cancel') %></button>
<button v-if="mode == 'edit'" class="btn btn-primary" v-on="click: onSubmit"><%= t('terms.save') %></button>
</p>
</div>
</div>
</script>

View File

@ -19,6 +19,7 @@ require "jquery-rails"
require "sucker_punch"
require "settingslogic"
require "kramdown-haml"
require "jbuilder"
module FluentdUi
class Application < Rails::Application

View File

@ -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

View File

@ -31,8 +31,10 @@ ja:
no_alert: 通知なし
update_password: パスワード更新
detail: 詳細
cancel: キャンセル
create: 作成
update: 更新
save: 保存
edit: 編集
destroy: 削除
new: 新規作成

View File

@ -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

View File

@ -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 }
<source>
type forward
port 24224
</source>
CONF
let(:new_config) { <<-CONF.strip_heredoc }
<source>
type http
port 8899
</source>
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

View File

@ -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

View File

@ -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