mirror of
https://github.com/fluent/fluentd-ui.git
synced 2026-05-05 19:06:12 +02:00
Merge pull request #105 from fluent/setting_component
Setting component
This commit is contained in:
commit
71e44aa9d1
112
app/assets/javascripts/vue/settings.js
Normal file
112
app/assets/javascripts/vue/settings.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
@ -150,3 +150,9 @@ label {
|
||||
.nav > li > a.section {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
|
||||
#vue-setting textarea {
|
||||
min-height: 12em;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
55
app/controllers/api/settings_controller.rb
Normal file
55
app/controllers/api/settings_controller.rb
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
6
app/views/api/settings/_element.json.jbuilder
Normal file
6
app/views/api/settings/_element.json.jbuilder
Normal 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
|
||||
3
app/views/api/settings/index.json.jbuilder
Normal file
3
app/views/api/settings/index.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
json.array! @config.elements do |elm|
|
||||
json.partial! "api/settings/element", element: elm
|
||||
end
|
||||
1
app/views/api/settings/show.json.jbuilder
Normal file
1
app/views/api/settings/show.json.jbuilder
Normal file
@ -0,0 +1 @@
|
||||
json.partial! "api/settings/element", element: @section
|
||||
@ -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
|
||||
|
||||
@ -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"}
|
||||
|
||||
23
app/views/shared/vue/_setting.html.erb
Normal file
23
app/views/shared/vue/_setting.html.erb
Normal 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>
|
||||
@ -19,6 +19,7 @@ require "jquery-rails"
|
||||
require "sucker_punch"
|
||||
require "settingslogic"
|
||||
require "kramdown-haml"
|
||||
require "jbuilder"
|
||||
|
||||
module FluentdUi
|
||||
class Application < Rails::Application
|
||||
|
||||
@ -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
|
||||
|
||||
@ -31,8 +31,10 @@ ja:
|
||||
no_alert: 通知なし
|
||||
update_password: パスワード更新
|
||||
detail: 詳細
|
||||
cancel: キャンセル
|
||||
create: 作成
|
||||
update: 更新
|
||||
save: 保存
|
||||
edit: 編集
|
||||
destroy: 削除
|
||||
new: 新規作成
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
21
spec/support/javascript_macro.rb
Normal file
21
spec/support/javascript_macro.rb
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user