ruby_cms 0.2.0.1 → 0.2.0.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/app/controllers/ruby_cms/admin/commands_controller.rb +122 -0
- data/app/javascript/controllers/ruby_cms/admin_commands_controller.js +72 -0
- data/app/javascript/controllers/ruby_cms/index.js +10 -0
- data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +21 -1
- data/app/services/ruby_cms/command_runner.rb +42 -0
- data/app/views/ruby_cms/admin/commands/index.html.erb +104 -0
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +12 -5
- data/app/views/ruby_cms/admin/settings/index.html.erb +9 -0
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +16 -0
- data/config/locales/nl.yml +17 -0
- data/config/routes.rb +3 -0
- data/lib/ruby_cms/commands_registry.rb +40 -0
- data/lib/ruby_cms/engine/navigation_registration.rb +10 -1
- data/lib/ruby_cms/settings_registry.rb +7 -0
- data/lib/ruby_cms/version.rb +1 -1
- data/lib/ruby_cms.rb +1 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6cb6a4534aa479ff1d6b82351e60cc965d544aa7770e28a8e97502ab03596874
|
|
4
|
+
data.tar.gz: 1d149a75c0e04968cfc49608e3224ed188d49fbe4ab127dbc0184ebad88d64b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bbca13f42f1062a96839fb2e378c3a63c94f44c27cec43e23db091bee4c40e252077f78f9b827e70317f58ecfe5740f4f304720585173b938cf302fda92649c
|
|
7
|
+
data.tar.gz: 2424fda47001767955be5fc4d9809e5f0f56dffedc096ac8b67583e2a29b4c76a7401bb2b82f9867784a5545bc2c2987862f429c419caaff7dac3e8e57154c72
|
data/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module Admin
|
|
5
|
+
class CommandsController < BaseController
|
|
6
|
+
cms_page :commands
|
|
7
|
+
|
|
8
|
+
# HTML: POST/redirect/GET — URL blijft /settings/commands (geen /run in de adresbalk).
|
|
9
|
+
RUN_CACHE_PREFIX = "ruby_cms/commands/run/v1/"
|
|
10
|
+
FLASH_RUN = :ruby_cms_commands_run
|
|
11
|
+
|
|
12
|
+
def index
|
|
13
|
+
@commands = visible_commands
|
|
14
|
+
consume_run_payload_from_flash
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
key = params.permit(:key)[:key].presence || params.dig(:command, :key)
|
|
19
|
+
cmd = RubyCms.find_command(key)
|
|
20
|
+
unless cmd
|
|
21
|
+
return respond_to do |format|
|
|
22
|
+
format.html do
|
|
23
|
+
redirect_to ruby_cms_admin_settings_commands_path,
|
|
24
|
+
alert: t("ruby_cms.admin.commands.unknown", default: "Unknown command.")
|
|
25
|
+
end
|
|
26
|
+
format.json { render json: { error: "Unknown command" }, status: :not_found }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require_permission!(cmd[:permission])
|
|
31
|
+
|
|
32
|
+
output = utf8_text(RubyCms::CommandRunner.run_rake(cmd[:rake_task]))
|
|
33
|
+
log_tail = utf8_text(RubyCms::CommandRunner.tail_log)
|
|
34
|
+
|
|
35
|
+
respond_to do |format|
|
|
36
|
+
format.html do
|
|
37
|
+
token = persist_run_payload_for_redirect(output: output, log_tail: log_tail)
|
|
38
|
+
redirect_to ruby_cms_admin_settings_commands_path,
|
|
39
|
+
status: :see_other,
|
|
40
|
+
flash: { FLASH_RUN => token }
|
|
41
|
+
end
|
|
42
|
+
format.json do
|
|
43
|
+
render json: {
|
|
44
|
+
command_output: output,
|
|
45
|
+
app_log_tail: log_tail
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
Rails.logger.error("[RubyCMS] Commands#run: #{e.class}: #{e.message}")
|
|
51
|
+
respond_to do |format|
|
|
52
|
+
format.html do
|
|
53
|
+
token = persist_run_payload_for_redirect(error: utf8_text(e.message))
|
|
54
|
+
redirect_to ruby_cms_admin_settings_commands_path,
|
|
55
|
+
status: :see_other,
|
|
56
|
+
flash: { FLASH_RUN => token }
|
|
57
|
+
end
|
|
58
|
+
format.json { render json: { error: e.message }, status: :unprocessable_entity }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def persist_run_payload_for_redirect(output: nil, log_tail: nil, error: nil)
|
|
65
|
+
payload = { output: output, log_tail: log_tail, error: error }.compact
|
|
66
|
+
token = SecureRandom.urlsafe_base64(32)
|
|
67
|
+
if rails_cache_null_store?
|
|
68
|
+
session[session_run_key(token)] = payload
|
|
69
|
+
else
|
|
70
|
+
Rails.cache.write("#{RUN_CACHE_PREFIX}#{token}", payload, expires_in: 15.minutes)
|
|
71
|
+
end
|
|
72
|
+
token
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def consume_run_payload_from_flash
|
|
76
|
+
token = flash[FLASH_RUN]
|
|
77
|
+
return if token.blank?
|
|
78
|
+
|
|
79
|
+
payload = load_run_payload(token)
|
|
80
|
+
assign_run_ivars_from_payload(payload) if payload.is_a?(Hash)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def load_run_payload(token)
|
|
84
|
+
if rails_cache_null_store?
|
|
85
|
+
session.delete(session_run_key(token))
|
|
86
|
+
else
|
|
87
|
+
key = "#{RUN_CACHE_PREFIX}#{token}"
|
|
88
|
+
data = Rails.cache.read(key)
|
|
89
|
+
Rails.cache.delete(key) if data
|
|
90
|
+
data
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def assign_run_ivars_from_payload(payload)
|
|
95
|
+
p = payload.with_indifferent_access
|
|
96
|
+
@run_error = utf8_text(p[:error]) if p[:error].present?
|
|
97
|
+
@run_command_output = utf8_text(p[:output]) if p.key?(:output)
|
|
98
|
+
@run_app_log_tail = utf8_text(p[:log_tail]) if p.key?(:log_tail)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def rails_cache_null_store?
|
|
102
|
+
Rails.cache.is_a?(ActiveSupport::Cache::NullStore)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def session_run_key(token)
|
|
106
|
+
"#{RUN_CACHE_PREFIX}#{token}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Rake / log IO is often ASCII-8BIT; ERB buffers are UTF-8 — normalize before render/JSON.
|
|
110
|
+
def utf8_text(value)
|
|
111
|
+
return +"" if value.nil?
|
|
112
|
+
|
|
113
|
+
value.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def visible_commands
|
|
117
|
+
RubyCms.registered_commands.select {|c| current_user_cms&.can?(c[:permission]) }
|
|
118
|
+
.sort_by {|c| [ c[:label].to_s.downcase, c[:key] ] }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["commandOutput", "appLog"];
|
|
5
|
+
static values = { runUrl: String };
|
|
6
|
+
|
|
7
|
+
async run(event) {
|
|
8
|
+
const key =
|
|
9
|
+
event.currentTarget?.dataset?.commandKey || event.params?.key;
|
|
10
|
+
const runUrl = this.runUrlValue?.trim?.() || this.runUrlValue;
|
|
11
|
+
|
|
12
|
+
if (!key || !runUrl) {
|
|
13
|
+
const msg =
|
|
14
|
+
!runUrl
|
|
15
|
+
? "Missing run URL (check data-ruby-cms--admin-commands-run-url-value)."
|
|
16
|
+
: "Missing command key on the button.";
|
|
17
|
+
if (this.hasCommandOutputTarget) {
|
|
18
|
+
this.commandOutputTarget.textContent = msg;
|
|
19
|
+
} else {
|
|
20
|
+
console.error("[ruby-cms--admin-commands]", msg);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const token = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
26
|
+
const button = event.currentTarget;
|
|
27
|
+
button.disabled = true;
|
|
28
|
+
|
|
29
|
+
if (this.hasCommandOutputTarget) {
|
|
30
|
+
this.commandOutputTarget.textContent = "Running…";
|
|
31
|
+
}
|
|
32
|
+
if (this.hasAppLogTarget) {
|
|
33
|
+
this.appLogTarget.textContent = "…";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(runUrl, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
Accept: "application/json",
|
|
42
|
+
"X-CSRF-Token": token || "",
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({ key }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const data = await response.json().catch(() => ({}));
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const err = data.error || `Request failed (${response.status})`;
|
|
51
|
+
if (this.hasCommandOutputTarget) {
|
|
52
|
+
this.commandOutputTarget.textContent = err;
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.hasCommandOutputTarget) {
|
|
58
|
+
this.commandOutputTarget.textContent =
|
|
59
|
+
data.command_output || "(no output)";
|
|
60
|
+
}
|
|
61
|
+
if (this.hasAppLogTarget) {
|
|
62
|
+
this.appLogTarget.textContent = data.app_log_tail || "(no log lines)";
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (this.hasCommandOutputTarget) {
|
|
66
|
+
this.commandOutputTarget.textContent = e?.message || String(e);
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
button.disabled = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -12,6 +12,7 @@ import ClickableRowController from "ruby_cms/clickable_row_controller";
|
|
|
12
12
|
import AutoSavePreferenceController from "ruby_cms/auto_save_preference_controller";
|
|
13
13
|
import NavOrderSortableController from "ruby_cms/nav_order_sortable_controller";
|
|
14
14
|
import ContentBlockHistoryController from "ruby_cms/content_block_history_controller";
|
|
15
|
+
import AdminCommandsController from "ruby_cms/admin_commands_controller";
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
export {
|
|
@@ -26,10 +27,16 @@ export {
|
|
|
26
27
|
AutoSavePreferenceController,
|
|
27
28
|
NavOrderSortableController,
|
|
28
29
|
ContentBlockHistoryController,
|
|
30
|
+
AdminCommandsController,
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
// Helper function to register all RubyCms controllers with a Stimulus application
|
|
34
|
+
const registeredApplications = new WeakSet();
|
|
35
|
+
|
|
32
36
|
export function registerRubyCmsControllers(application) {
|
|
37
|
+
if (!application || typeof application.register !== "function") return;
|
|
38
|
+
if (registeredApplications.has(application)) return;
|
|
39
|
+
|
|
33
40
|
application.register("ruby-cms--visual-editor", VisualEditorController);
|
|
34
41
|
application.register("ruby-cms--page-preview", PagePreviewController);
|
|
35
42
|
application.register("ruby-cms--mobile-menu", MobileMenuController);
|
|
@@ -41,6 +48,7 @@ export function registerRubyCmsControllers(application) {
|
|
|
41
48
|
application.register("ruby-cms--toggle", ToggleController);
|
|
42
49
|
application.register("ruby-cms--locale-tabs", LocaleTabsController);
|
|
43
50
|
application.register("ruby-cms--content-block-history", ContentBlockHistoryController);
|
|
51
|
+
application.register("ruby-cms--admin-commands", AdminCommandsController);
|
|
44
52
|
application.register("clickable-row", ClickableRowController);
|
|
45
53
|
application.register(
|
|
46
54
|
"ruby-cms--auto-save-preference",
|
|
@@ -50,6 +58,8 @@ export function registerRubyCmsControllers(application) {
|
|
|
50
58
|
"ruby-cms--nav-order-sortable",
|
|
51
59
|
NavOrderSortableController,
|
|
52
60
|
);
|
|
61
|
+
|
|
62
|
+
registeredApplications.add(application);
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
// Auto-register controllers when this module is imported
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = ["tab"];
|
|
4
|
+
static targets = ["tab", "localeLabel"];
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
const activeTab = this.tabTargets.find(
|
|
8
|
+
(t) => t.getAttribute("aria-selected") === "true"
|
|
9
|
+
);
|
|
10
|
+
if (!activeTab) return;
|
|
11
|
+
|
|
12
|
+
this.tabTargets.forEach((t) => {
|
|
13
|
+
this.setTabState(t, t === activeTab);
|
|
14
|
+
});
|
|
15
|
+
this.syncLocaleLabel(activeTab);
|
|
16
|
+
}
|
|
5
17
|
|
|
6
18
|
switchTab(event) {
|
|
7
19
|
const tab = event.currentTarget;
|
|
@@ -25,6 +37,7 @@ export default class extends Controller {
|
|
|
25
37
|
|
|
26
38
|
// Highlight selected tab
|
|
27
39
|
this.setTabState(tab, true);
|
|
40
|
+
this.syncLocaleLabel(tab);
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
findPanel(panelId) {
|
|
@@ -47,6 +60,13 @@ export default class extends Controller {
|
|
|
47
60
|
tab.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
48
61
|
}
|
|
49
62
|
|
|
63
|
+
syncLocaleLabel(tab) {
|
|
64
|
+
if (!this.hasLocaleLabelTarget) return;
|
|
65
|
+
|
|
66
|
+
const label = tab.dataset.localeLabel;
|
|
67
|
+
if (label) this.localeLabelTarget.textContent = label;
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
splitClasses(classListString) {
|
|
51
71
|
return (classListString || "")
|
|
52
72
|
.split(" ")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module RubyCms
|
|
6
|
+
# Runs whitelisted Rake tasks and reads log tails for the admin Commands UI.
|
|
7
|
+
class CommandRunner
|
|
8
|
+
class << self
|
|
9
|
+
def run_rake(task)
|
|
10
|
+
raise ArgumentError, "rake task blank" if task.to_s.strip.blank?
|
|
11
|
+
|
|
12
|
+
env = { "RAILS_ENV" => Rails.env.to_s }
|
|
13
|
+
argv = [ "bundle", "exec", "rake", task.to_s ]
|
|
14
|
+
stdout_and_stderr, status = Open3.capture2e(env, *argv, chdir: Rails.root.to_s)
|
|
15
|
+
<<~TEXT.strip
|
|
16
|
+
$ #{argv.join(" ")}
|
|
17
|
+
(exit #{status.exitstatus})
|
|
18
|
+
|
|
19
|
+
#{stdout_and_stderr}
|
|
20
|
+
TEXT
|
|
21
|
+
rescue Errno::ENOENT => e
|
|
22
|
+
<<~TEXT.strip
|
|
23
|
+
Failed to run command: #{e.message}
|
|
24
|
+
TEXT
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tail_log(lines: 400, max_bytes: 512 * 1024)
|
|
28
|
+
path = Rails.root.join("log", "#{Rails.env}.log")
|
|
29
|
+
return "(Log file not found: #{path})" unless path.file?
|
|
30
|
+
|
|
31
|
+
File.open(path, "rb") do |f|
|
|
32
|
+
size = f.size
|
|
33
|
+
f.seek([ 0, size - max_bytes ].max)
|
|
34
|
+
chunk = f.read
|
|
35
|
+
chunk.lines.last(lines.to_i.clamp(1, 10_000)).join
|
|
36
|
+
end
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
"(Could not read log: #{e.message})"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<%= render RubyCms::Admin::AdminPageHeader.new(
|
|
2
|
+
title: t("ruby_cms.admin.commands.title", default: "Commands"),
|
|
3
|
+
subtitle: t("ruby_cms.admin.commands.subtitle", default: "Run registered Rake tasks from the host app. Output and recent log lines appear on the right."),
|
|
4
|
+
breadcrumbs: [
|
|
5
|
+
{ label: t("ruby_cms.admin.breadcrumb.admin", default: "Admin"), url: ruby_cms_admin_root_path },
|
|
6
|
+
{ label: t("ruby_cms.admin.commands.title", default: "Commands") }
|
|
7
|
+
]
|
|
8
|
+
) %>
|
|
9
|
+
|
|
10
|
+
<div class="flex min-h-0 flex-col gap-4">
|
|
11
|
+
<% if @run_error.present? %>
|
|
12
|
+
<div class="w-full shrink-0 rounded-xl border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
13
|
+
<%= h(@run_error) %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<%#
|
|
18
|
+
md+: 3-koloms grid — links 1/3 (commando's), rechts 2/3 (output + log).
|
|
19
|
+
Klein scherm: één kolom, eerst commando's, daaronder resultaat.
|
|
20
|
+
%>
|
|
21
|
+
<div
|
|
22
|
+
class="grid min-h-0 w-full min-w-0 grid-cols-1 gap-4 md:grid-cols-3 md:gap-6 md:items-stretch md:min-h-[min(32rem,calc(100dvh-14rem))] md:max-h-[min(90vh,calc(100dvh-12rem))] md:overflow-hidden"
|
|
23
|
+
dir="ltr"
|
|
24
|
+
>
|
|
25
|
+
<%# Links: 1/3 — beschikbare commando's %>
|
|
26
|
+
<aside
|
|
27
|
+
class="flex min-h-[16rem] w-full min-w-0 flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm ring-1 ring-black/[0.03] md:col-span-1 md:row-start-1 md:h-full md:min-h-0"
|
|
28
|
+
>
|
|
29
|
+
<div class="shrink-0 border-b border-border/80 px-4 py-3">
|
|
30
|
+
<h2 class="text-sm font-semibold leading-tight tracking-tight text-foreground">
|
|
31
|
+
<%= t("ruby_cms.admin.commands.available", default: "Available commands") %>
|
|
32
|
+
</h2>
|
|
33
|
+
<p class="mt-1 text-xs leading-snug text-muted-foreground">
|
|
34
|
+
<%= t("ruby_cms.admin.commands.hint", default: "Commands are registered in the host app initializer (RubyCms.register_command).") %>
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4">
|
|
38
|
+
<% if @commands.any? %>
|
|
39
|
+
<div class="flex flex-col gap-3">
|
|
40
|
+
<% @commands.each do |cmd| %>
|
|
41
|
+
<div class="rounded-xl border border-border/60 bg-muted/20 p-3">
|
|
42
|
+
<div class="flex flex-col gap-2">
|
|
43
|
+
<div class="min-w-0">
|
|
44
|
+
<p class="text-sm font-medium leading-snug text-foreground"><%= cmd[:label] %></p>
|
|
45
|
+
<% if cmd[:description].present? %>
|
|
46
|
+
<p class="mt-1 text-xs leading-snug text-muted-foreground"><%= cmd[:description] %></p>
|
|
47
|
+
<% end %>
|
|
48
|
+
<p class="mt-2 break-all font-mono text-[10px] leading-tight text-muted-foreground">
|
|
49
|
+
rake <%= cmd[:rake_task] %>
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
<%= button_to t("ruby_cms.admin.commands.run", default: "Run"),
|
|
53
|
+
ruby_cms_admin_settings_commands_run_path,
|
|
54
|
+
method: :post,
|
|
55
|
+
params: { key: cmd[:key] },
|
|
56
|
+
form: {
|
|
57
|
+
data: { turbo: false },
|
|
58
|
+
class: "m-0 w-full shrink-0"
|
|
59
|
+
},
|
|
60
|
+
class: "inline-flex w-full cursor-pointer items-center justify-center rounded-lg bg-primary px-3 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90" %>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
<% else %>
|
|
66
|
+
<p class="text-sm text-muted-foreground">
|
|
67
|
+
<%= t("ruby_cms.admin.commands.empty", default: "No commands are registered, or your role cannot run any of them.") %>
|
|
68
|
+
</p>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
71
|
+
</aside>
|
|
72
|
+
|
|
73
|
+
<%# Rechts: 2/3 — commando-output + applicatielog %>
|
|
74
|
+
<section
|
|
75
|
+
aria-label="<%= t("ruby_cms.admin.commands.right_column_label", default: "Command result and application log") %>"
|
|
76
|
+
class="flex min-h-0 w-full min-w-0 flex-col gap-3 md:col-span-2 md:row-start-1 md:h-full md:min-h-0 md:overflow-hidden md:border-l md:border-border/60 md:pl-6"
|
|
77
|
+
>
|
|
78
|
+
<%# min-h-0: grid/flex item mag krimpen zodat max-h geldt en <pre> scrollt i.p.v. de pagina. %>
|
|
79
|
+
<div class="flex min-h-0 max-h-[50dvh] flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm ring-1 ring-black/[0.03]">
|
|
80
|
+
<div class="shrink-0 border-b border-border/80 px-4 py-3">
|
|
81
|
+
<h3 class="text-sm font-semibold text-foreground">
|
|
82
|
+
<%= t("ruby_cms.admin.commands.command_output", default: "Command output") %>
|
|
83
|
+
</h3>
|
|
84
|
+
</div>
|
|
85
|
+
<pre
|
|
86
|
+
class="ruby-cms-commands__pre min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-contain whitespace-pre-wrap break-words p-4 text-xs font-mono text-foreground [scrollbar-gutter:stable]"
|
|
87
|
+
aria-live="polite"
|
|
88
|
+
><%= @run_command_output.present? ? h(@run_command_output) : t("ruby_cms.admin.commands.output_placeholder", default: "Output appears here after you run a command.") %></pre>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="flex min-h-0 max-h-[50dvh] flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm ring-1 ring-black/[0.03] [scrollbar-gutter:stable]">
|
|
92
|
+
<div class="shrink-0 border-b border-border/80 px-4 py-3">
|
|
93
|
+
<h3 class="text-sm font-semibold text-foreground">
|
|
94
|
+
<%= t("ruby_cms.admin.commands.app_log", default: "Application log (tail)") %>
|
|
95
|
+
</h3>
|
|
96
|
+
</div>
|
|
97
|
+
<pre
|
|
98
|
+
class="ruby-cms-commands__pre min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-contain whitespace-pre-wrap break-words p-4 text-xs font-mono text-muted-foreground [scrollbar-gutter:stable]"
|
|
99
|
+
aria-live="polite"
|
|
100
|
+
><%= @run_app_log_tail.present? ? h(@run_app_log_tail) : t("ruby_cms.admin.commands.log_placeholder", default: "Recent log lines appear here after a run.") %></pre>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
id: "content-block-form",
|
|
29
29
|
data: { turbo: false } do |f| %>
|
|
30
30
|
<div class="<%= RubyCms::Admin::AdminResourceCard::CARD_CLASS %>">
|
|
31
|
-
<div class="<%= RubyCms::Admin::AdminResourceCard::GRID_CLASS %>"
|
|
31
|
+
<div class="<%= RubyCms::Admin::AdminResourceCard::GRID_CLASS %>"
|
|
32
|
+
data-controller="ruby-cms--locale-tabs">
|
|
32
33
|
<%# ── Form Fields (Left 2/3) ── %>
|
|
33
34
|
<div class="<%= RubyCms::Admin::AdminResourceCard::MAIN_CLASS %>">
|
|
34
35
|
<% if @content_block.errors.any? %>
|
|
@@ -78,18 +79,23 @@
|
|
|
78
79
|
|
|
79
80
|
<%# ── Locale Tabs ── %>
|
|
80
81
|
<% if @blocks_by_locale.present? %>
|
|
81
|
-
<div class="rounded-xl border border-border/60 overflow-hidden"
|
|
82
|
+
<div class="rounded-xl border border-border/60 overflow-hidden">
|
|
82
83
|
<div class="flex gap-1 p-2 bg-background border-b border-border/60" role="tablist">
|
|
83
84
|
<% @blocks_by_locale.each_with_index do |(locale_s, _), idx| %>
|
|
85
|
+
<% tab_active = "bg-background text-primary shadow-sm ring-1 ring-border/60" %>
|
|
86
|
+
<% tab_inactive = "bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground" %>
|
|
84
87
|
<button type="button"
|
|
85
88
|
role="tab"
|
|
86
|
-
aria-selected="<%= idx == 0 %>"
|
|
89
|
+
aria-selected="<%= idx == 0 ? "true" : "false" %>"
|
|
87
90
|
aria-controls="locale-panel-<%= locale_s %>"
|
|
88
91
|
id="locale-tab-<%= locale_s %>"
|
|
89
92
|
data-ruby-cms--locale-tabs-target="tab"
|
|
90
93
|
data-panel-id="locale-panel-<%= locale_s %>"
|
|
94
|
+
data-locale-label="<%= ruby_cms_locale_display_name(locale_s) %>"
|
|
95
|
+
data-active-classes="<%= tab_active %>"
|
|
96
|
+
data-inactive-classes="<%= tab_inactive %>"
|
|
91
97
|
data-action="click->ruby-cms--locale-tabs#switchTab"
|
|
92
|
-
class="px-3 py-1.5 rounded-md text-xs font-medium border-none cursor-pointer transition-colors <%= idx == 0 ?
|
|
98
|
+
class="px-3 py-1.5 rounded-md text-xs font-medium border-none cursor-pointer transition-colors <%= idx == 0 ? tab_active : tab_inactive %>">
|
|
93
99
|
<%= ruby_cms_locale_display_name(locale_s) %>
|
|
94
100
|
</button>
|
|
95
101
|
<% end %>
|
|
@@ -154,7 +160,8 @@
|
|
|
154
160
|
|
|
155
161
|
<div>
|
|
156
162
|
<dt class="<%= RubyCms::Admin::AdminResourceCard::DETAIL_LABEL_CLASS %>">Locale</dt>
|
|
157
|
-
<dd class="<%= RubyCms::Admin::AdminResourceCard::DETAIL_VALUE_CLASS %>"
|
|
163
|
+
<dd class="<%= RubyCms::Admin::AdminResourceCard::DETAIL_VALUE_CLASS %>"
|
|
164
|
+
data-ruby-cms--locale-tabs-target="localeLabel"><%= ruby_cms_locale_display_name(@content_block.locale) %></dd>
|
|
158
165
|
</div>
|
|
159
166
|
|
|
160
167
|
<div>
|
|
@@ -80,6 +80,15 @@
|
|
|
80
80
|
<% end %>
|
|
81
81
|
</div>
|
|
82
82
|
</div>
|
|
83
|
+
|
|
84
|
+
<div class="mt-4 rounded-xl border border-border/80 bg-muted/15 px-4 py-3 text-sm leading-relaxed text-muted-foreground">
|
|
85
|
+
<p class="m-0"><%= t("ruby_cms.admin.settings.commands_page_hint") %></p>
|
|
86
|
+
<p class="mt-2 mb-0">
|
|
87
|
+
<%= link_to t("ruby_cms.admin.settings.commands_open_link"),
|
|
88
|
+
ruby_cms_admin_settings_commands_path,
|
|
89
|
+
class: "font-medium text-foreground underline decoration-border underline-offset-2 hover:text-primary" %>
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
83
92
|
<% elsif entries_for_grid.any? %>
|
|
84
93
|
<h2 class="text-base font-semibold tracking-tight text-foreground"><%= settings_tab_label(@active_tab) %></h2>
|
|
85
94
|
<p class="mt-1 text-sm text-muted-foreground">
|
data/config/importmap.rb
CHANGED
|
@@ -36,3 +36,5 @@ pin "ruby_cms/nav_order_sortable_controller",
|
|
|
36
36
|
pin "ruby_cms/page_preview_controller", to: "controllers/ruby_cms/page_preview_controller.js"
|
|
37
37
|
pin "ruby_cms/content_block_history_controller",
|
|
38
38
|
to: "controllers/ruby_cms/content_block_history_controller.js"
|
|
39
|
+
pin "ruby_cms/admin_commands_controller",
|
|
40
|
+
to: "controllers/ruby_cms/admin_commands_controller.js"
|
data/config/locales/en.yml
CHANGED
|
@@ -27,6 +27,7 @@ en:
|
|
|
27
27
|
permissions: "Permissions"
|
|
28
28
|
visitor_errors: "Visitor errors"
|
|
29
29
|
users: "Users"
|
|
30
|
+
commands: "Commands"
|
|
30
31
|
settings: "Settings"
|
|
31
32
|
admin:
|
|
32
33
|
breadcrumb:
|
|
@@ -40,6 +41,19 @@ en:
|
|
|
40
41
|
de: "German"
|
|
41
42
|
fr: "French"
|
|
42
43
|
es: "Spanish"
|
|
44
|
+
commands:
|
|
45
|
+
title: "Commands"
|
|
46
|
+
subtitle: "Run registered Rake tasks from the host app. Output and recent log lines appear on the right."
|
|
47
|
+
available: "Available commands"
|
|
48
|
+
hint: "Commands are registered in the host app initializer (RubyCms.register_command)."
|
|
49
|
+
run: "Run"
|
|
50
|
+
empty: "No commands are registered, or your role cannot run any of them."
|
|
51
|
+
command_output: "Command output"
|
|
52
|
+
app_log: "Application log (tail)"
|
|
53
|
+
output_placeholder: "Output appears here after you run a command."
|
|
54
|
+
log_placeholder: "Recent log lines appear here after a run."
|
|
55
|
+
unknown: "Unknown command."
|
|
56
|
+
right_column_label: "Command result and application log"
|
|
43
57
|
settings:
|
|
44
58
|
title: "Settings"
|
|
45
59
|
subtitle: "Configure admin interface preferences"
|
|
@@ -57,6 +71,8 @@ en:
|
|
|
57
71
|
reset_inline_hint: "Restore factory defaults for the entire admin (all sections)."
|
|
58
72
|
reset_confirm: "Reset all settings to default values?"
|
|
59
73
|
no_settings: "No settings in this category"
|
|
74
|
+
commands_page_hint: "Registered rake tasks (Commands) are not a tab on this screen. Open “Commands” in the left sidebar under “Settings”, or use the link below."
|
|
75
|
+
commands_open_link: "Open Commands"
|
|
60
76
|
analytics:
|
|
61
77
|
title: "Analytics"
|
|
62
78
|
subtitle: "Traffic and engagement overview"
|
data/config/locales/nl.yml
CHANGED
|
@@ -16,6 +16,7 @@ nl:
|
|
|
16
16
|
permissions: "Rechten"
|
|
17
17
|
visitor_errors: "Bezoekersfouten"
|
|
18
18
|
users: "Gebruikers"
|
|
19
|
+
commands: "Commando's"
|
|
19
20
|
settings: "Instellingen"
|
|
20
21
|
admin:
|
|
21
22
|
breadcrumb:
|
|
@@ -29,6 +30,22 @@ nl:
|
|
|
29
30
|
de: "Duits"
|
|
30
31
|
fr: "Frans"
|
|
31
32
|
es: "Spaans"
|
|
33
|
+
commands:
|
|
34
|
+
title: "Commando's"
|
|
35
|
+
subtitle: "Voer geregistreerde Rake-taken uit (host app). Uitvoer en recente logregels staan rechts."
|
|
36
|
+
available: "Beschikbare commando's"
|
|
37
|
+
hint: "Commando's registreer je in de host app (RubyCms.register_command)."
|
|
38
|
+
run: "Uitvoeren"
|
|
39
|
+
empty: "Er zijn geen commando's geregistreerd, of je rol mag er geen uitvoeren."
|
|
40
|
+
command_output: "Uitvoer commando"
|
|
41
|
+
app_log: "Applicatielog (tail)"
|
|
42
|
+
output_placeholder: "Uitvoer verschijnt hier na het uitvoeren."
|
|
43
|
+
log_placeholder: "Recente logregels verschijnen hier na een run."
|
|
44
|
+
unknown: "Onbekend commando."
|
|
45
|
+
right_column_label: "Commando-resultaat en applicatielog"
|
|
46
|
+
settings:
|
|
47
|
+
commands_page_hint: "Geregistreerde Rake-taken (Commando's) staan niet als tab op dit scherm. Open «Commando's» in de linkerzijbalk onder het kopje Instellingen, of gebruik de link hieronder."
|
|
48
|
+
commands_open_link: "Commando's openen"
|
|
32
49
|
content_blocks:
|
|
33
50
|
created: "Contentblok aangemaakt."
|
|
34
51
|
updated: "Contentblok bijgewerkt."
|
data/config/routes.rb
CHANGED
|
@@ -49,6 +49,9 @@ RubyCms::Engine.routes.draw do # rubocop:disable Metrics/BlockLength
|
|
|
49
49
|
post "settings/nav_order", to: "settings#update_nav_order", as: :settings_nav_order
|
|
50
50
|
post "settings/reset_defaults", to: "settings#reset_defaults", as: :settings_reset_defaults
|
|
51
51
|
|
|
52
|
+
get "settings/commands", to: "commands#index", as: :settings_commands
|
|
53
|
+
post "settings/commands/run", to: "commands#run", as: :settings_commands_run
|
|
54
|
+
|
|
52
55
|
resources :users, only: %i[index create destroy] do
|
|
53
56
|
collection do
|
|
54
57
|
delete "bulk_delete", to: "users#bulk_delete"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
# Host apps register runnable commands (usually Rake tasks) shown on Admin → Commands.
|
|
5
|
+
module CommandsRegistry
|
|
6
|
+
def registered_commands
|
|
7
|
+
@registered_commands ||= []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def registered_commands=(list)
|
|
11
|
+
@registered_commands = list
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Register a button-triggered command for the admin Commands screen.
|
|
15
|
+
#
|
|
16
|
+
# @param key [String, Symbol] Unique id (e.g. :copy_en_to_nl)
|
|
17
|
+
# @param label [String] Button label
|
|
18
|
+
# @param rake_task [String] Full rake task name, including args in brackets if needed
|
|
19
|
+
# @param description [String, nil] Optional help text under the button
|
|
20
|
+
# @param permission [Symbol] Required to run (+can?+); default +:manage_admin+
|
|
21
|
+
def register_command(key:, label:, rake_task:, description: nil, permission: :manage_admin)
|
|
22
|
+
k = key.to_s
|
|
23
|
+
entry = {
|
|
24
|
+
key: k,
|
|
25
|
+
label: label.to_s,
|
|
26
|
+
rake_task: rake_task.to_s,
|
|
27
|
+
description: description.to_s.presence,
|
|
28
|
+
permission: permission.to_sym
|
|
29
|
+
}
|
|
30
|
+
self.registered_commands = registered_commands.reject {|e| e[:key] == k } + [entry]
|
|
31
|
+
entry
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_command(key)
|
|
35
|
+
registered_commands.find {|e| e[:key] == key.to_s }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
extend CommandsRegistry
|
|
40
|
+
end
|
|
@@ -66,6 +66,15 @@ module RubyCms
|
|
|
66
66
|
permission: :manage_permissions,
|
|
67
67
|
order: 4
|
|
68
68
|
)
|
|
69
|
+
RubyCms.register_page(
|
|
70
|
+
key: :commands,
|
|
71
|
+
label: "Commands",
|
|
72
|
+
path: lambda(&:ruby_cms_admin_settings_commands_path),
|
|
73
|
+
icon: :wrench,
|
|
74
|
+
section: :settings,
|
|
75
|
+
permission: :manage_admin,
|
|
76
|
+
order: 5
|
|
77
|
+
)
|
|
69
78
|
RubyCms.register_page(
|
|
70
79
|
key: :settings,
|
|
71
80
|
label: "Settings",
|
|
@@ -73,7 +82,7 @@ module RubyCms
|
|
|
73
82
|
icon: :cog_6_tooth,
|
|
74
83
|
section: :settings,
|
|
75
84
|
permission: :manage_admin,
|
|
76
|
-
order:
|
|
85
|
+
order: 6
|
|
77
86
|
)
|
|
78
87
|
end
|
|
79
88
|
end
|
|
@@ -240,6 +240,13 @@ module RubyCms
|
|
|
240
240
|
category: :navigation,
|
|
241
241
|
description: "Show Analytics in navigation"
|
|
242
242
|
)
|
|
243
|
+
register(
|
|
244
|
+
key: :nav_show_commands,
|
|
245
|
+
type: :boolean,
|
|
246
|
+
default: true,
|
|
247
|
+
category: :navigation,
|
|
248
|
+
description: "Show Commands in navigation"
|
|
249
|
+
)
|
|
243
250
|
register(
|
|
244
251
|
key: :nav_order,
|
|
245
252
|
type: :json,
|
data/lib/ruby_cms/version.rb
CHANGED
data/lib/ruby_cms.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "ruby_cms/engine"
|
|
|
10
10
|
require_relative "ruby_cms/app_integration"
|
|
11
11
|
require_relative "ruby_cms/content_blocks_sync"
|
|
12
12
|
require_relative "ruby_cms/content_blocks_grouping"
|
|
13
|
+
require_relative "ruby_cms/commands_registry"
|
|
13
14
|
|
|
14
15
|
module RubyCms
|
|
15
16
|
class Error < StandardError; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_cms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.0.
|
|
4
|
+
version: 0.2.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Codebyjob
|
|
@@ -96,6 +96,7 @@ files:
|
|
|
96
96
|
- app/controllers/concerns/ruby_cms/visitor_error_capture.rb
|
|
97
97
|
- app/controllers/ruby_cms/admin/analytics_controller.rb
|
|
98
98
|
- app/controllers/ruby_cms/admin/base_controller.rb
|
|
99
|
+
- app/controllers/ruby_cms/admin/commands_controller.rb
|
|
99
100
|
- app/controllers/ruby_cms/admin/content_block_versions_controller.rb
|
|
100
101
|
- app/controllers/ruby_cms/admin/content_blocks_controller.rb
|
|
101
102
|
- app/controllers/ruby_cms/admin/dashboard_controller.rb
|
|
@@ -114,6 +115,7 @@ files:
|
|
|
114
115
|
- app/helpers/ruby_cms/bulk_action_table_helper.rb
|
|
115
116
|
- app/helpers/ruby_cms/content_blocks_helper.rb
|
|
116
117
|
- app/helpers/ruby_cms/settings_helper.rb
|
|
118
|
+
- app/javascript/controllers/ruby_cms/admin_commands_controller.js
|
|
117
119
|
- app/javascript/controllers/ruby_cms/auto_save_preference_controller.js
|
|
118
120
|
- app/javascript/controllers/ruby_cms/bulk_action_table_controller.js
|
|
119
121
|
- app/javascript/controllers/ruby_cms/clickable_row_controller.js
|
|
@@ -138,6 +140,7 @@ files:
|
|
|
138
140
|
- app/models/ruby_cms/user_permission.rb
|
|
139
141
|
- app/models/ruby_cms/visitor_error.rb
|
|
140
142
|
- app/services/ruby_cms/analytics/report.rb
|
|
143
|
+
- app/services/ruby_cms/command_runner.rb
|
|
141
144
|
- app/services/ruby_cms/security_tracker.rb
|
|
142
145
|
- app/views/admin/content_block_versions/index.html.erb
|
|
143
146
|
- app/views/admin/content_block_versions/show.html.erb
|
|
@@ -160,6 +163,7 @@ files:
|
|
|
160
163
|
- app/views/ruby_cms/admin/analytics/partials/_top_visitors.html.erb
|
|
161
164
|
- app/views/ruby_cms/admin/analytics/partials/_utm_sources.html.erb
|
|
162
165
|
- app/views/ruby_cms/admin/analytics/visitor_details.html.erb
|
|
166
|
+
- app/views/ruby_cms/admin/commands/index.html.erb
|
|
163
167
|
- app/views/ruby_cms/admin/content_block_versions/index.html.erb
|
|
164
168
|
- app/views/ruby_cms/admin/content_block_versions/show.html.erb
|
|
165
169
|
- app/views/ruby_cms/admin/content_blocks/_form.html.erb
|
|
@@ -215,6 +219,7 @@ files:
|
|
|
215
219
|
- lib/ruby_cms.rb
|
|
216
220
|
- lib/ruby_cms/app_integration.rb
|
|
217
221
|
- lib/ruby_cms/cli.rb
|
|
222
|
+
- lib/ruby_cms/commands_registry.rb
|
|
218
223
|
- lib/ruby_cms/content_blocks_grouping.rb
|
|
219
224
|
- lib/ruby_cms/content_blocks_sync.rb
|
|
220
225
|
- lib/ruby_cms/css_compiler.rb
|