command_proposal 1.0.8 → 1.0.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36c34ec1bf4f4ad80708120c0f2add544b8bb576c1087fc34b0ea7bc537c07ae
4
- data.tar.gz: 86877f325b5eb60d7949d6145304229a1da644a87109b8fc8fdf0961a5b484cb
3
+ metadata.gz: ab0271a9e0e80bf0df78d8284cc8e13d292ef2254ab57180b1434f50b351dcb7
4
+ data.tar.gz: 21b077ee6cfa9ede8ea83315523297ff9268bf5236de797c9f3acae9f1ef3910
5
5
  SHA512:
6
- metadata.gz: 642066c9c9c36200f1ee0873b8017c501aa3950a4d880d94ef5adfd959841775ea74c4beeedc96f9212e346ea067868f8c33656524e962c9cae8d33d17ae4565
7
- data.tar.gz: 8d4bc91c68b050bbd6d79433aa5aaadd824e00ebdb7e2fe9c73a54fff31f339fa3b248f88b0dcaec259492c3319406a731b7561e6aceaaa25fd1a099c94cc59e
6
+ metadata.gz: 52a46842e394c99d8dbb4527c222ab93e93dc437992996b3b9af3850a9bc6571bac4e472268c7f821d6b541c17074d21954d0b18e5e15d2109141a4f04179491
7
+ data.tar.gz: eb670b929efd2d6c612997e569276fac67aa494325db38d357a5589496bdba19b0d5335d68696d715022b45e9ed13eae4e2245d4ddc12f53371ad3d611e4987b
@@ -141,6 +141,16 @@ cmdDocReady(function() {
141
141
  line.textContent = console_input.textContent
142
142
 
143
143
  console_input.textContent = ""
144
+
145
+ var result = document.createElement("div")
146
+ result.classList.add("result")
147
+
148
+ var spinner = document.createElement("i")
149
+ spinner.className = "fa fa-circle-o-notch fa-spin cmd-icon-grey"
150
+ result.append(spinner)
151
+
152
+ line.appendChild(result)
153
+
144
154
  lines.appendChild(line)
145
155
  stored_entry = undefined
146
156
  history_cmd_idx = undefined
@@ -167,6 +177,8 @@ cmdDocReady(function() {
167
177
  done: function(res, status, req) {
168
178
  if (status == 200) {
169
179
  var json = JSON.parse(res)
180
+ line.querySelector(".result").remove()
181
+
170
182
  var result = document.createElement("div")
171
183
  result.classList.add("result")
172
184
 
@@ -174,7 +186,21 @@ cmdDocReady(function() {
174
186
  result.classList.add("cmd-error")
175
187
  result.textContent = json.error
176
188
  } else {
177
- result.textContent = json.result
189
+ var truncate = 2000
190
+ if (json.result.length > truncate-3) {
191
+ result.textContent = json.result.slice(0, truncate-3) + "..."
192
+ var encoded = encodeURIComponent(json.result)
193
+
194
+ var download = document.createElement("a")
195
+ download.classList.add("cmd-truncated-download")
196
+ download.setAttribute("href", "data:application/txt," + encoded)
197
+ download.setAttribute("download", "result.txt")
198
+ download.textContent = "Output truncated. Click here to download full result."
199
+
200
+ line.insertAdjacentElement("afterend", download)
201
+ } else {
202
+ result.textContent = json.result
203
+ }
178
204
  }
179
205
 
180
206
  line.appendChild(result)
@@ -29,6 +29,7 @@ cmdDocReady(function() {
29
29
  }
30
30
  document.querySelector("td[data-iteration-status]").innerText = json.status
31
31
  document.querySelector("td[data-iteration-duration]").innerText = json.duration
32
+ document.querySelector("td[data-iteration-started]").innerText = json.started_at
32
33
 
33
34
  if (continue_statuses.includes(json.status)) {
34
35
  setTimeout(function() { pingFeed(terminal) }, 1000)
@@ -4,13 +4,13 @@ cmdDocReady(function() {
4
4
  function setReadOnlyUI(cm) {
5
5
  cm.getWrapperElement().classList.add("CodeMirror-readonly")
6
6
 
7
- pencil = document.createElement("i")
7
+ var pencil = document.createElement("i")
8
8
  pencil.className = "fa fa-pencil fa-stack-1x"
9
9
 
10
- ban = document.createElement("i")
10
+ var ban = document.createElement("i")
11
11
  ban.className = "fa fa-ban fa-stack-2x fa-flip-horizontal"
12
12
 
13
- stack = document.createElement("span")
13
+ var stack = document.createElement("span")
14
14
  stack.className = "fa-stack fa-2x"
15
15
  stack.append(pencil)
16
16
  stack.append(ban)
@@ -6,6 +6,12 @@
6
6
  width: 100%;
7
7
  }
8
8
 
9
+ .cmd-truncated-download {
10
+ font-size: 12px;
11
+ color: grey;
12
+ text-decoration: underline;
13
+ }
14
+
9
15
  .cmd-flex-row {
10
16
  display: flex;
11
17
  }
@@ -6,14 +6,12 @@
6
6
  background: #112435;
7
7
  padding: 10px 20px;
8
8
  padding-left: 40px;
9
- overflow-x: auto;
10
- overflow-y: visible;
11
9
  color: lime;
12
10
  font-family: monospace;
13
11
  text-align: left;
14
- white-space: nowrap;
15
- white-space: pre;
16
12
  counter-reset: line-count;
13
+ white-space: pre-wrap;
14
+ word-wrap: break-word;
17
15
 
18
16
  .line {
19
17
  min-height: 18px;
@@ -67,6 +65,3 @@
67
65
  color: black;
68
66
  }
69
67
  }
70
- .cmd-terminal {
71
- white-space: pre-wrap;
72
- }
@@ -28,7 +28,6 @@ class ::CommandProposal::IterationsController < ::CommandProposal::EngineControl
28
28
 
29
29
  return error!("Session has expired. Please start a new session.") if runner.nil?
30
30
 
31
-
32
31
  @task.user = command_user # Separate from update to ensure it's set first
33
32
  @task.update(code: params[:code]) # Creates a new iteration
34
33
  @iteration = @task.current_iteration
@@ -65,6 +65,7 @@ class ::CommandProposal::RunnerController < ::CommandProposal::EngineController
65
65
  result: @iteration.result,
66
66
  status: @iteration.status,
67
67
  duration: humanized_duration(@iteration.duration),
68
+ started_at: @iteration.started_at&.strftime("%b %-d '%y, %-l:%M%P")
68
69
  }.tap do |response|
69
70
  if @iteration.started?
70
71
  response[:endpoint] = runner_url(@task, @iteration)
@@ -51,6 +51,7 @@ class ::CommandProposal::TasksController < ::CommandProposal::EngineController
51
51
  def create
52
52
  @task = ::CommandProposal::Task.new(task_params.except(:code))
53
53
  @task.user = command_user
54
+ @task.skip_approval = true unless approval_required?
54
55
 
55
56
  # Cannot create the iteration until the task is created, so save then update
56
57
  if @task.save && @task.update(task_params)
@@ -69,6 +70,7 @@ class ::CommandProposal::TasksController < ::CommandProposal::EngineController
69
70
  def update
70
71
  @task = ::CommandProposal::Task.find_by!(friendly_id: params[:id])
71
72
  @task.user = command_user
73
+ @task.skip_approval = true unless approval_required?
72
74
 
73
75
  if @task.update(task_params)
74
76
  redirect_to cmd_path(@task)
@@ -48,10 +48,7 @@ module CommandProposal
48
48
  end
49
49
 
50
50
  def engine
51
- @engine ||= begin
52
- name = `rails routes | grep command_proposal_engine`[/\w*command_proposal_engine/]
53
- send(name)
54
- end
51
+ @engine ||= send(::CommandProposal.engine_name)
55
52
  end
56
53
 
57
54
  # Runner controller doesn't map to a model, so needs special handling
@@ -2,14 +2,14 @@ module CommandProposal
2
2
  module PermissionsHelper
3
3
  def can_command?(user=command_user)
4
4
  return false unless permitted_to_use?
5
- return true unless cmd_config.approval_required?
5
+ return true unless approval_required?
6
6
 
7
7
  command_user.try("#{cmd_config.role_scope}?")
8
8
  end
9
9
 
10
10
  def can_approve?(iteration)
11
11
  return false unless permitted_to_use?
12
- return true unless cmd_config.approval_required?
12
+ return true unless approval_required?
13
13
  return if iteration.nil?
14
14
 
15
15
  command_user.try("#{cmd_config.role_scope}?") && !current_is_author?(iteration)
@@ -17,11 +17,15 @@ module CommandProposal
17
17
 
18
18
  def has_approval?(task)
19
19
  return false unless permitted_to_use?
20
- return true unless cmd_config.approval_required?
20
+ return true unless approval_required?
21
21
 
22
22
  task&.approved?
23
23
  end
24
24
 
25
+ def approval_required?
26
+ cmd_config.approval_required?
27
+ end
28
+
25
29
  def current_is_author?(iteration)
26
30
  return false unless permitted_to_use?
27
31
 
@@ -1,4 +1,7 @@
1
1
  module CommandProposal
2
2
  class ApplicationJob < ActiveJob::Base
3
+ rescue_from(StandardError) do |exception|
4
+ Rails.logger.error "[#{self.class.name}] Job failed and will not retry: #{exception.to_s}"
5
+ end
3
6
  end
4
7
  end
@@ -21,6 +21,9 @@ class ::CommandProposal::Iteration < ApplicationRecord
21
21
  serialize :args, ::CommandProposal::Service::JSONWrapper
22
22
  include ::CommandProposal::Service::ExternalBelong
23
23
 
24
+ TRUNCATE_COUNT = 2000
25
+ # Also hardcoded in JS: app/assets/javascripts/command_proposal/console.js
26
+
24
27
  has_many :comments
25
28
  belongs_to :task
26
29
  external_belongs_to :requester
@@ -70,6 +73,10 @@ class ::CommandProposal::Iteration < ApplicationRecord
70
73
  (completed_at || stopped_at || Time.current) - started_at
71
74
  end
72
75
 
76
+ def end_time
77
+ completed_at || stopped_at || Time.current
78
+ end
79
+
73
80
  def force_reset
74
81
  # Debugging method. Should never actually be called.
75
82
  update(status: :approved, result: nil, completed_at: nil, stopped_at: nil, started_at: nil)
@@ -7,7 +7,7 @@
7
7
 
8
8
  class ::CommandProposal::Task < ApplicationRecord
9
9
  self.table_name = :command_proposal_tasks
10
- attr_accessor :user
10
+ attr_accessor :user, :skip_approval
11
11
 
12
12
  has_many :iterations
13
13
  has_many :ordered_iterations, -> { order(created_at: :desc) }, class_name: "CommandProposal::Iteration"
@@ -79,7 +79,17 @@ class ::CommandProposal::Task < ApplicationRecord
79
79
  end
80
80
 
81
81
  def code=(new_code)
82
- iterations.create(code: new_code, requester: user)
82
+ if skip_approval
83
+ iterations.create(
84
+ code: new_code,
85
+ requester: user,
86
+ status: :approved,
87
+ approver: user,
88
+ approved_at: Time.current
89
+ )
90
+ else
91
+ iterations.create(code: new_code, requester: user)
92
+ end
83
93
  end
84
94
 
85
95
  private
@@ -1,8 +1,18 @@
1
1
  <% if lines.none? && !(skip_empty ||= false) -%><div class="line"></div><% end
2
2
  -%><% lines.each do |iteration| -%>
3
- <div class="line"><%= iteration.code -%><%
4
- if iteration.result.present?
5
- -%><div class="result"><%= iteration.result %></div><%
6
- end
7
- -%></div>
8
- <% end -%>
3
+ <div class="line"><%= iteration.code
4
+ -%><div class="result"><%=
5
+ truncate = ::CommandProposal::Iteration::TRUNCATE_COUNT
6
+ if iteration.result.present?
7
+ iteration.result.truncate(truncate)
8
+ elsif iteration.completed?
9
+ "Error: No response"
10
+ else
11
+ content_tag :i, nil, class: "fa fa-circle-o-notch fa-spin cmd-icon-grey"
12
+ end
13
+ %></div
14
+ ></div><%=
15
+ if iteration.result.length > truncate
16
+ link_to("Output truncated. Click here to download full result.", "data:application/txt,#{ERB::Util.url_encode(iteration.result)}", class: "cmd-truncated-download", download: "result.txt")
17
+ end
18
+ %><% end -%>
@@ -1,4 +1,8 @@
1
1
  <% if lines.blank? && !(skip_empty ||= false) -%><div class="line"></div><% end
2
2
  -%><% lines&.split("\n").each do |line|
3
- -%><div class="line"><%= line -%></div><%
3
+ truncate = ::CommandProposal::Iteration::TRUNCATE_COUNT
4
+ -%><div class="line"><%= line.truncate(truncate) -%></div><%=
5
+ if line.length > truncate
6
+ link_to("Output truncated. Click here to download full result.", "data:application/txt,#{ERB::Util.url_encode(line)}", class: "cmd-truncated-download", download: "result.txt")
7
+ end -%><%
4
8
  end -%>
@@ -18,8 +18,8 @@
18
18
  <% end %>
19
19
  </td>
20
20
  <td><%= iteration.status.capitalize %></td>
21
- <!-- <td><%= iteration.comments.count %></td> -->
22
- <!-- <td><%= link_to "Diff", cmd_path(@task, iteration: @iteration.id, diff: iteration.id) %></td> -->
21
+ <!-- <td><%#= iteration.comments.count %></td> -->
22
+ <!-- <td><%#= link_to "Diff", cmd_path(@task, iteration: @iteration.id, diff: iteration.id) %></td> -->
23
23
  </tr>
24
24
  <% end %>
25
25
  </tbody>
@@ -10,7 +10,7 @@
10
10
  <tr>
11
11
  <td><%= @iteration&.requester_name.presence || "ID: #{@iteration&.requester_id}" if @iteration&.requester_id.present? %></td>
12
12
  <td><%= @iteration&.approver_name.presence || "ID: #{@iteration&.approver_id}" if @iteration&.approver_id.present? %></td>
13
- <td><%= @iteration&.started_at&.strftime("%b %-d '%y, %-l:%M%P") %></td>
13
+ <td data-iteration-started><%= @iteration&.started_at&.strftime("%b %-d '%y, %-l:%M%P") %></td>
14
14
  <td data-iteration-status><%= @iteration&.status&.capitalize %></td>
15
15
  <td data-iteration-duration><%= humanized_duration(@iteration&.duration) %></td>
16
16
  </tr>
@@ -66,7 +66,7 @@ module CommandProposal
66
66
  # Rollback the create/update if anything fails
67
67
  ActiveRecord::Base.transaction do
68
68
  command_request if @task.function? && @iteration.approved_at? && @iteration.complete?
69
- @iteration.update(@params)
69
+ @iteration.update(@params.merge(requester: @user))
70
70
 
71
71
  error!("Cannot run without approval.") unless has_approval?(@task)
72
72
  end
@@ -91,7 +91,8 @@ module CommandProposal
91
91
  if ::CommandProposal.sessions.key?("task:#{@task.id}")
92
92
  @task.first_iteration.update(status: :success, completed_at: Time.current)
93
93
  else
94
- @task.first_iteration.update(status: :terminated, completed_at: Time.current)
94
+ ended_at = @task.iterations.last&.end_time || Time.current
95
+ @task.first_iteration.update(status: :terminated, completed_at: ended_at)
95
96
  end
96
97
  ::CommandProposal.sessions.delete("task:#{@task.id}")
97
98
  end
@@ -10,6 +10,31 @@ module CommandProposal
10
10
  new.execute(task.primary_iteration)
11
11
  end
12
12
 
13
+ def self.command(friendly_id, user, params={})
14
+ # Hack magic because requires are not playing well with spring
15
+ require "command_proposal/services/command_interpreter"
16
+
17
+ params = params.to_unsafe_h if params.is_a?(ActionController::Parameters)
18
+
19
+ iteration = ::CommandProposal::Services::CommandInterpreter.command(
20
+ ::CommandProposal::Task.find_by!(friendly_id: friendly_id).primary_iteration,
21
+ :run,
22
+ user,
23
+ { args: params }
24
+ )
25
+
26
+ start = Time.current
27
+ wait_time = 5 # seconds
28
+ loop do
29
+ sleep 0.4
30
+
31
+ break if iteration.reload.complete?
32
+ break if Time.current - start > wait_time
33
+ end
34
+
35
+ iteration
36
+ end
37
+
13
38
  def initialize
14
39
  @session = session
15
40
  end
@@ -66,6 +91,7 @@ module CommandProposal
66
91
  # Run bring functions in here so we can capture any string outputs
67
92
  # OR! Run the full runner and instead of saving to an iteration, return the string for prepending here
68
93
  result = @session.eval("_ = (#{@iteration.code})").inspect # rubocop:disable Security/Eval - Eval is scary, but in this case it's exactly what we need.
94
+ result = nil unless @iteration.task.console? # Only store final result for consoles
69
95
  status = :success
70
96
  rescue Exception => e # rubocop:disable Lint/RescueException - Yes, rescue full Exception so that we can catch typos in evals as well
71
97
  status = :failed
@@ -95,7 +121,7 @@ module CommandProposal
95
121
 
96
122
  $stdout = stored_stdout
97
123
  @iteration.status = status
98
- @iteration.result = [output, "#{result || 'nil'}"].compact.join("\n")
124
+ @iteration.result = [output, result].compact.join("\n")
99
125
  end
100
126
 
101
127
  def bring_function
@@ -1,3 +1,3 @@
1
1
  module CommandProposal
2
- VERSION = "1.0.8"
2
+ VERSION = "1.0.12"
3
3
  end
@@ -22,6 +22,12 @@ module CommandProposal
22
22
  @configuration = ::CommandProposal::Configuration.new
23
23
  end
24
24
 
25
+ def self.engine_name
26
+ @engine_name ||= begin
27
+ `rails routes | grep command_proposal_engine`[/\w*command_proposal_engine/]
28
+ end
29
+ end
30
+
25
31
  def self.configure
26
32
  yield(configuration)
27
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: command_proposal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.8
4
+ version: 1.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rocco Nicholls
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-14 00:00:00.000000000 Z
11
+ date: 2021-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.0'
19
+ version: 5.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5.0'
26
+ version: 5.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: font-awesome-rails
29
29
  requirement: !ruby/object:Gem::Requirement