lit 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a051d3c6a1e4a1db24c3818aae84d51f42df73a907c23b787f6f5b3c40f798aa
4
- data.tar.gz: e080032aebf0428fdfd9ca2bc3a0c75964774b774c1caed2329cbb337d70cd9e
3
+ metadata.gz: 4d3b3569eb99f453aea4b65451511bdeb76680774b664ab108f445359b9e10f0
4
+ data.tar.gz: cbf55f8eefb23231f5201087e612a58172e2ce4537fc0c8211a597ebba8e5f88
5
5
  SHA512:
6
- metadata.gz: 679930cd3c4ee3d0c506dce93b471b8df4edbaac0772ef05d0a9caac3878d284d467599234bed06f77876af6dff2a25bded7f8a1f223bfbabfde2ddfe8ab7ed1
7
- data.tar.gz: 5c6908710836b9f2c11a5da6933e616883e8fdb8ff497908c5cd2ad37c4279ab2b558fb46b1b87f07ae35adceb11143e093ac82393244cf9b6cf440ad4d79a84
6
+ metadata.gz: be93a781bdea6ff7a095bd86a60fc671c5f325f6d6f37a45bfea883b549f3655c4d9d5e768b125181501b944303dccc89d3032180952f4f870a8878bc7d6816d
7
+ data.tar.gz: 8655233c8aba69f06bf775bd28df3bc7e17f81f4378dab0f8818c6fac05e22e521ef966c5ae3706dfc3f2552d747687d22c241422f51aeab1f34764bc30bd698
data/README.md CHANGED
@@ -37,10 +37,10 @@ gem 'lit'
37
37
 
38
38
  2. run `bundle install`
39
39
 
40
- 3. run installation generator `bundle exec rails g lit:install`
41
- (for production/staging environment `redis` is suggested as key value engine. `hash` will not work in multi process environment)
40
+ 3. Add `config.i18n.available_locales = [...]` to `application.rb` - it's required to precompile appropriate language flags in lit backend.
42
41
 
43
- 4. Add `config.i18n.available_locales = [...]` to `application.rb` - it's required to precompile appropriate language flags in lit backend.
42
+ 4. run installation generator `bundle exec rails g lit:install`
43
+ (for production/staging environment `redis` is suggested as key value engine. `hash` will not work in multi process environment)
44
44
 
45
45
  5. After doing above and restarting app, point your browser to `http://app/lit`
46
46
 
@@ -145,10 +145,10 @@ These credentials can be given in three ways:
145
145
  ... # see Google docs link above for reference
146
146
  }
147
147
  end
148
-
148
+
149
149
  # For example, for Rails 6, from encrypted credentials file (HashWithIndifferentAccess is used, because the keys in
150
150
  the credentials.config could be strings or, as well, symbols):
151
-
151
+
152
152
  Lit::CloudTranslation.configure do |config|
153
153
  config.keyfile_hash = HashWithIndifferentAccess.new(Rails.application.credentials.config[:google_translate_api])
154
154
  end
@@ -2,25 +2,32 @@
2
2
  $(document).ready ->
3
3
  $('td.localization_row[data-editing=0]').on 'click', ->
4
4
  $this = $(this)
5
- if parseInt($this.data('editing'))==0
5
+ if parseInt($this.data('editing')) == 0
6
6
  edited_rows[$this.data('id')] = $this.html()
7
7
  unless parseInt($this.data('editing'))
8
8
  $this.data('editing', '1')
9
9
  $.get $this.data('edit')
10
- $('td.localization_row').on 'click', 'form button.cancel', (e)->
10
+ $('td.localization_row').on 'click', 'form button.cancel', (e) ->
11
11
  $this = $(this)
12
- if $this[0].localName=='button'
12
+ if $this[0].localName == 'button'
13
13
  $this = $this.parents('td.localization_row')
14
14
  $this.data('editing', 0)
15
15
  $this.html edited_rows[$this.data('id')]
16
16
  e.preventDefault()
17
17
  false
18
- $('tr.localization_versions_row').on 'click', '.close_versions', (e)->
18
+ $('tr.localization_versions_row').on 'click', '.close_versions', (e) ->
19
19
  $this = $(this)
20
20
  $parent = $this.parents('tr.localization_versions_row')
21
21
  $parent.addClass('hidden')
22
22
  $parent.children('td').html('')
23
- $('tr.localization_key_row').on 'click', 'input.wysiwyg_switch', (e)->
23
+ $('tr.localization_key_row').on 'click', 'input.wysiwyg_switch', (e) ->
24
24
  $(this).parents('form').find("textarea").jqte()
25
- $('tr.localization_key_row').on 'click', '.request_info_link', (e)->
25
+ $('tr.localization_key_row').on 'click', '.request_info_link', (e) ->
26
26
  $(this).parents('tr.localization_key_row').find(".request_info_row").toggleClass('hidden')
27
+ $('tr.localization_key_row').on 'click', '.js-copy_to_clipboard', (e) ->
28
+ if(!navigator.clipboard)
29
+ alert('No browser support for clipboard')
30
+ else
31
+ navigator.clipboard.writeText($(this).data('key'))
32
+ e.preventDefault()
33
+ false
@@ -11,17 +11,17 @@
11
11
  *= require './backend/jquery-te-1.4.0.css'
12
12
  *= require_self
13
13
  */
14
- .detail_wrapper{
15
- padding: 10px 0 0 50px;
14
+ .detail_wrapper {
15
+ padding: 10px 0 0 10px;
16
16
  }
17
- .detail_wrapper table tr td.locale_row{
17
+ .detail_wrapper table tr td.locale_row {
18
18
  width: 75px;
19
19
  }
20
- .localization_key_row .localization_keys_options{
20
+ .localization_key_row .localization_keys_options {
21
21
  display: none;
22
22
  float: right;
23
23
  }
24
- .localization_key_row:hover .localization_keys_options{
24
+ .localization_key_row:hover .localization_keys_options {
25
25
  display: block;
26
26
  }
27
27
 
@@ -29,29 +29,29 @@ li.key_prefix .fa-chevron-right {
29
29
  float: right;
30
30
  margin-top: 2px;
31
31
  margin-right: -6px;
32
- opacity: .25;
32
+ opacity: 0.25;
33
33
  }
34
34
 
35
- .hidden{
35
+ .hidden {
36
36
  display: none;
37
37
  }
38
- i.fa{
38
+ i.fa {
39
39
  color: black;
40
40
  }
41
- .nav.nav-stacked>li>a {
42
- padding: 5px 7px;
41
+ .nav.nav-stacked > li > a {
42
+ padding: 5px 7px;
43
43
  }
44
- .well{
44
+ .well {
45
45
  background-color: white;
46
46
  border-radius: 0px;
47
47
  }
48
- .well label{
48
+ .well label {
49
49
  font-weight: normal;
50
50
  }
51
51
  .well .form-search {
52
52
  margin-bottom: 15px;
53
53
  }
54
- .localization_row em{
54
+ .localization_row em {
55
55
  color: #bbb;
56
56
  }
57
57
  .loading {
@@ -60,3 +60,6 @@ padding: 5px 7px;
60
60
  .loaded {
61
61
  display: none;
62
62
  }
63
+ .copy_to_clipboard_btn {
64
+ cursor: pointer;
65
+ }
@@ -18,7 +18,7 @@ module Lit
18
18
  from: params[:from],
19
19
  to: @target_localization.locale.locale
20
20
  }.compact
21
- @translated_text = Lit::CloudTranslation.translate(opts)
21
+ @translated_text = Lit::CloudTranslation.translate(**opts)
22
22
  rescue Lit::CloudTranslation::TranslationError => e
23
23
  @error_message = "Translation failed. #{e.message}"
24
24
  end
@@ -1,17 +1,23 @@
1
1
  module Lit
2
- module Concerns
3
- module RequestInfoStore
4
- extend ::ActiveSupport::Concern
5
- included do
6
- before_action :store_request_path
7
- end
2
+ module RequestInfoStore
3
+ extend ::ActiveSupport::Concern
4
+ included do
5
+ before_action :store_request_path
6
+ end
8
7
 
9
- private
8
+ private
10
9
 
11
- def store_request_path
12
- Thread.current[:lit_current_request_path] = request&.path
13
- end
10
+ def store_request_path
11
+ Thread.current[:lit_current_request_path] = request&.path
14
12
  end
15
13
  end
16
14
  end
17
15
 
16
+ module Lit::Concerns::RequestInfoStore
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ Rails.logger.info 'DEPRECATED: Use include Lit::RequestInfoStore'
21
+ include Lit::RequestInfoStore
22
+ end
23
+ end
@@ -1,16 +1,23 @@
1
1
  module Lit
2
- module Concerns
3
- module RequestKeysStore
4
- extend ::ActiveSupport::Concern
5
- included do
6
- before_action :init_request_keys
7
- end
2
+ module RequestKeysStore
3
+ extend ::ActiveSupport::Concern
4
+ included do
5
+ before_action :init_request_keys
6
+ end
8
7
 
9
- private
8
+ private
10
9
 
11
- def init_request_keys
12
- Thread.current[:lit_request_keys] = {}
13
- end
10
+ def init_request_keys
11
+ Thread.current[:lit_request_keys] = {}
14
12
  end
15
13
  end
16
14
  end
15
+
16
+ module Lit::Concerns::RequestKeysStore
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ Rails.logger.info 'DEPRECATED: Use include Lit::RequestKeysStore'
21
+ include Lit::RequestKeysStore
22
+ end
23
+ end
@@ -1,9 +1,7 @@
1
1
  module Lit
2
2
  class LocalizationKeysController < ::Lit::ApplicationController
3
- before_action :find_localization_scope,
4
- except: %i[destroy find_localization]
5
- before_action :find_localization_key,
6
- only: %i[star destroy change_completed restore_deleted]
3
+ before_action :find_localization_scope, except: %i[destroy find_localization]
4
+ before_action :find_localization_key, only: %i[star destroy change_completed restore_deleted]
7
5
 
8
6
  def index
9
7
  get_localization_keys
@@ -20,27 +18,19 @@ module Lit
20
18
  end
21
19
 
22
20
  def find_localization
23
- localization_key = Lit::LocalizationKey.find_by!(
24
- localization_key: params[:key]
25
- )
21
+ localization_key = Lit::LocalizationKey.find_by!(localization_key: params[:key])
26
22
  locale = Lit::Locale.find_by!(locale: params[:locale])
27
23
  localization = localization_key.localizations.find_by(locale_id: locale)
28
- render json: {
29
- path: localization_key_localization_path(localization_key, localization)
30
- }
24
+ render json: { path: localization_key_localization_path(localization_key, localization) }
31
25
  end
32
26
 
33
27
  def starred
34
28
  @scope = @scope.where(is_starred: true)
35
29
 
36
- if defined?(Kaminari) &&
37
- @scope.respond_to?(Kaminari.config.page_method_name)
30
+ if defined?(Kaminari) && @scope.respond_to?(Kaminari.config.page_method_name)
38
31
  @scope = @scope.send(Kaminari.config.page_method_name, params[:page])
39
32
  end
40
- if defined?(WillPaginate) &&
41
- @scope.respond_to?(:paginate)
42
- @scope = @scope.paginate(page: params[:page])
43
- end
33
+ @scope = @scope.paginate(page: params[:page]) if defined?(WillPaginate) && @scope.respond_to?(:paginate)
44
34
  get_localization_keys
45
35
  render action: :index
46
36
  end
@@ -66,6 +56,16 @@ module Lit
66
56
  respond_to :js
67
57
  end
68
58
 
59
+ def batch_touch
60
+ get_localization_keys
61
+ localization_key_ids = @scope.distinct(false).pluck(:id)
62
+ if localization_key_ids.any?
63
+ @scope.distinct(false).update_all updated_at: Time.current
64
+ Localization.where(localization_key_id: localization_key_ids).update_all updated_at: Time.current
65
+ end
66
+ respond_to :js
67
+ end
68
+
69
69
  private
70
70
 
71
71
  def find_localization_key
@@ -73,19 +73,16 @@ module Lit
73
73
  end
74
74
 
75
75
  def find_localization_scope
76
- @search_options = if params.respond_to?(:permit)
77
- params.permit(*valid_keys)
78
- else
79
- params.slice(*valid_keys)
80
- end
81
- @scope = LocalizationKey.distinct.active
82
- .preload(localizations: :locale)
83
- .search(@search_options)
76
+ @search_options = params.respond_to?(:permit) ? params.permit(*valid_keys) : params.slice(*valid_keys)
77
+ @scope = LocalizationKey.distinct.active.preload(localizations: :locale).search(@search_options)
84
78
  end
85
79
 
86
80
  def get_localization_keys
87
81
  key_parts = @search_options[:key_prefix].to_s.split('.').length
88
- @prefixes = @scope.reorder(nil).distinct.pluck(:localization_key).map { |lk| lk.split('.').shift(key_parts + 1).join('.') }.uniq.sort
82
+ @prefixes =
83
+ @scope.reorder(nil).distinct.pluck(:localization_key).map do |lk|
84
+ lk.split('.').shift(key_parts + 1).join('.')
85
+ end.uniq.sort
89
86
  if @search_options[:key_prefix].present?
90
87
  parts = @search_options[:key_prefix].split('.')
91
88
  @parent_prefix = parts[0, parts.length - 1].join('.')
@@ -104,16 +101,15 @@ module Lit
104
101
  end
105
102
 
106
103
  def grouped_localizations
107
- @_grouped_localizations ||= begin
108
- {}.tap do |hash|
109
- @localization_keys.each do |lk|
110
- hash[lk] = {}
111
- lk.localizations.each do |l|
112
- hash[lk][l.locale.locale.to_sym] = l
104
+ @_grouped_localizations ||=
105
+ begin
106
+ {}.tap do |hash|
107
+ @localization_keys.each do |lk|
108
+ hash[lk] = {}
109
+ lk.localizations.each { |l| hash[lk][l.locale.locale.to_sym] = l }
113
110
  end
114
111
  end
115
112
  end
116
- end
117
113
  end
118
114
 
119
115
  def localization_for(locale, localization_key)
@@ -136,12 +132,16 @@ module Lit
136
132
  helper_method :localization_for
137
133
 
138
134
  def versions?(localization)
139
- @_versions ||= begin
140
- ids = grouped_localizations.values.map(&:values).flatten.map(&:id)
141
- Lit::Localization.active.where(id: ids).joins(:versions).group(
142
- "#{Lit::Localization.quoted_table_name}.id"
143
- ).count
144
- end
135
+ @_versions ||=
136
+ begin
137
+ ids = grouped_localizations.values.map(&:values).flatten.map(&:id)
138
+ Lit::Localization
139
+ .active
140
+ .where(id: ids)
141
+ .joins(:versions)
142
+ .group("#{Lit::Localization.quoted_table_name}.id")
143
+ .count
144
+ end
145
145
  @_versions[localization.id].to_i > 0
146
146
  end
147
147
  helper_method :versions?
@@ -9,7 +9,7 @@ module Lit
9
9
  key = scope_key_by_partial(key)
10
10
  key = pluralized_key(key, count) if count
11
11
 
12
- content = super(key, options.symbolize_keys)
12
+ content = super(key, **options.symbolize_keys)
13
13
  if !options[:skip_lit] && lit_authorized?
14
14
  content = get_translateable_span(key, content)
15
15
  end
@@ -21,7 +21,7 @@ class Lit::Base < ActiveRecord::Base
21
21
 
22
22
  private
23
23
 
24
- def create_or_update(*args, &block)
24
+ def create_or_update(**kwargs, &block)
25
25
  @was_saved_with_insert = true if new_record?
26
26
  @was_saved_with_update = true if persisted?
27
27
 
@@ -1,8 +1,12 @@
1
- <table class="table">
1
+ <div class="col-12 text-right">
2
+ <%= link_to "batch touch", lit.batch_touch_localization_keys_path(key: params[:key], key_prefix: params[:key_prefix]), method: :post, remote: true, class: 'btn btn-sm btn-primary', data: { confirm: 'This will "touch" all search results making them subject of synchronization. Proceed?'} %>
3
+ </div>
4
+ <table class="table mt-1">
2
5
  <%- @localization_keys.each do |lk| %>
3
6
  <tr class="localization_key_row" data-id="<%= lk.id %>">
4
7
  <td>
5
8
  <strong><%= lk.localization_key %></strong>
9
+ <i class="fa fa-clipboard js-copy_to_clipboard copy_to_clipboard_btn " data-key="<%= lk.localization_key %>"></i>
6
10
  <span class="badge"><%= Lit.init.cache.get_global_hits_counter(lk.localization_key) %></span>
7
11
  <div class="localization_keys_options">
8
12
  <% if Lit.store_request_info %>
@@ -30,26 +34,30 @@
30
34
  <div class="detail_wrapper">
31
35
  <table class="table table-bordered table-striped">
32
36
  <tr>
33
- <th class="col-md-8">Translation</th>
34
- <th class="col-md-2 text-center">Locale</th>
37
+ <th class="col-md-1"></th>
38
+ <th class="col-md-9">Translation</th>
39
+ <th class="col-md-1 text-center">Locale</th>
35
40
  <% unless lk.is_deleted? %>
36
- <th class="col-md-2 text-center">Completed</th>
41
+ <th class="col-md-1 text-center">Completed</th>
37
42
  <% end %>
38
43
  </tr>
39
44
  <%- available_locales.each do |locale| %>
40
45
  <%- localization = localization_for(locale, lk) %>
41
46
  <tr>
47
+ <td>
48
+ <% if localization %>
49
+ <%= draw_icon 'clock-o', title: "Last updated at #{localization.updated_at.to_s(:db)}" %>
50
+ <%= link_to lit.previous_versions_localization_key_localization_path(lk, localization, format: :js), class: "js-show_prev_versions #{'hidden' unless versions?(localization)}", remote: true do %>
51
+ <%= draw_icon 'random', title: I18n.t('lit.common.previous_versions', default: 'Previous versions') %>
52
+ <% end %>
53
+ <% end %>
54
+ </td>
42
55
  <td class="localization_row" data-id="<%= localization.id%>" data-edit="<%= edit_localization_key_localization_path(lk, localization, format: :js) %>" data-editing=0 data-content="">
43
56
  <%= render partial: 'localization_row', locals: {localization: Lit.init.cache["#{locale}.#{lk.localization_key}"]} %>
44
57
  </td>
45
58
  <td class="locale_row text-center">
46
59
  <%= EmojiFlag.new(locale) %>
47
60
  <%= locale %>
48
- <% if localization %>
49
- <%= link_to lit.previous_versions_localization_key_localization_path(lk, localization, format: :js), class: "show_prev_versions #{'hidden' unless versions?(localization)}", remote: true do %>
50
- <%= draw_icon 'random', title: I18n.t('lit.common.previous_versions', default: 'Previous versions') %>
51
- <% end %>
52
- <% end %>
53
61
  </td>
54
62
  <% unless lk.is_deleted? %>
55
63
  <td class="text-center">
@@ -65,7 +73,7 @@
65
73
  <% end %>
66
74
  <% if Lit.store_request_info %>
67
75
  <tr class="hidden request_info_row">
68
- <td colspan="2">
76
+ <td colspan="3">
69
77
  <strong>Translation key recently displayed on following pages:</strong>
70
78
  <ul>
71
79
  <% Lit.init.cache.get_request_info(lk.localization_key).split(' ').reverse.each do |l| %>
@@ -0,0 +1 @@
1
+ alert('All of search results have been marked as updated now, please retry synchronizing now');
@@ -1,6 +1,6 @@
1
1
  var $row = $('td.localization_row[data-id="<%= @localization.id %>"]');
2
2
  $row.data('editing', 0);
3
3
  $row.html("<%= ejs render(:partial=>"/lit/localization_keys/localization_row", formats: ['html'], :locals=>{:localization=>@localization.translated_value }) %>");
4
- $row.siblings().find('.show_prev_versions').removeClass('hidden');
4
+ $row.siblings().find('.js-show_prev_versions').removeClass('hidden');
5
5
  $('a.change_completed_<%= @localization.id %> input[type=checkbox]').prop("checked", true);
6
6
 
data/config/routes.rb CHANGED
@@ -25,6 +25,7 @@ Lit::Engine.routes.draw do
25
25
  get :find_localization
26
26
  get :not_translated
27
27
  get :visited_again
28
+ post :batch_touch
28
29
  end
29
30
  resources :localizations, only: [:edit, :update, :show] do
30
31
  member do
data/lib/lit/engine.rb CHANGED
@@ -3,6 +3,7 @@ module Lit
3
3
  require 'jquery-rails'
4
4
 
5
5
  config.autoload_paths += %W[#{Lit::Engine.root}/app/controllers/lit/concerns]
6
+ paths.add 'lib', eager_load: true # Zeitwerk compatibility
6
7
 
7
8
  isolate_namespace Lit
8
9
 
data/lib/lit/import.rb CHANGED
@@ -3,14 +3,14 @@ require 'csv'
3
3
  module Lit
4
4
  class Import
5
5
  class << self
6
- def call(*args)
7
- new(*args).perform
6
+ def call(**kwargs)
7
+ new(**kwargs).perform
8
8
  end
9
9
  end
10
10
 
11
11
  attr_reader :input, :locale_keys, :format, :skip_nil
12
12
 
13
- def initialize(input:, locale_keys: [], format:, skip_nil: true, dry_run: false, raw: false)
13
+ def initialize(input:, format:, locale_keys: [], skip_nil: true, dry_run: false, raw: false)
14
14
  raise ArgumentError, 'format must be yaml or csv' if %i[yaml csv].exclude?(format.to_sym)
15
15
  @input = input
16
16
  @locale_keys = locale_keys.presence || I18n.available_locales
@@ -143,7 +143,7 @@ module Lit
143
143
  .find_by('localization_key = ? and locale = ?', key, locale)
144
144
 
145
145
  return unless existing_translation
146
-
146
+
147
147
  if @raw
148
148
  existing_translation.update(default_value: value)
149
149
  else
data/lib/lit/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Lit
2
- VERSION = '1.1.1'.freeze
2
+ VERSION = '1.1.2'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Litwiniuk
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2021-03-08 00:00:00.000000000 Z
14
+ date: 2021-04-26 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rails
@@ -89,14 +89,14 @@ dependencies:
89
89
  requirements:
90
90
  - - "~>"
91
91
  - !ruby/object:Gem::Version
92
- version: 2.2.0
92
+ version: 2.4.0
93
93
  type: :development
94
94
  prerelease: false
95
95
  version_requirements: !ruby/object:Gem::Requirement
96
96
  requirements:
97
97
  - - "~>"
98
98
  - !ruby/object:Gem::Version
99
- version: 2.2.0
99
+ version: 2.4.0
100
100
  - !ruby/object:Gem::Dependency
101
101
  name: devise
102
102
  requirement: !ruby/object:Gem::Requirement
@@ -272,6 +272,7 @@ files:
272
272
  - app/views/lit/localization_keys/_localization_row.html.erb
273
273
  - app/views/lit/localization_keys/_localizations_list.html.erb
274
274
  - app/views/lit/localization_keys/_sidebar.html.erb
275
+ - app/views/lit/localization_keys/batch_touch.js.erb
275
276
  - app/views/lit/localization_keys/change_completed.js.erb
276
277
  - app/views/lit/localization_keys/destroy.js.erb
277
278
  - app/views/lit/localization_keys/index.html.erb
@@ -344,7 +345,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
344
345
  - !ruby/object:Gem::Version
345
346
  version: '0'
346
347
  requirements: []
347
- rubygems_version: 3.0.3
348
+ rubygems_version: 3.1.6
348
349
  signing_key:
349
350
  specification_version: 4
350
351
  summary: Database powered i18n backend with web gui