command_proposal 1.0.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +77 -0
- data/Rakefile +18 -0
- data/app/assets/config/command_proposal_manifest.js +1 -0
- data/app/assets/javascripts/command_proposal/_codemirror.js +9814 -0
- data/app/assets/javascripts/command_proposal/_helpers.js +9 -0
- data/app/assets/javascripts/command_proposal/codemirror-addon-searchcursor.js +296 -0
- data/app/assets/javascripts/command_proposal/codemirror-keymap-sublime.js +720 -0
- data/app/assets/javascripts/command_proposal/codemirror-mode-ruby.js +303 -0
- data/app/assets/javascripts/command_proposal/console.js +195 -0
- data/app/assets/javascripts/command_proposal/feed.js +51 -0
- data/app/assets/javascripts/command_proposal/terminal.js +40 -0
- data/app/assets/javascripts/command_proposal.js +1 -0
- data/app/assets/stylesheets/command_proposal/_variables.scss +0 -0
- data/app/assets/stylesheets/command_proposal/codemirror-rubyblue.scss +27 -0
- data/app/assets/stylesheets/command_proposal/codemirror.scss +367 -0
- data/app/assets/stylesheets/command_proposal/command_proposal.scss +1 -0
- data/app/assets/stylesheets/command_proposal/components.scss +31 -0
- data/app/assets/stylesheets/command_proposal/containers.scss +4 -0
- data/app/assets/stylesheets/command_proposal/icons.scss +12 -0
- data/app/assets/stylesheets/command_proposal/tables.scss +76 -0
- data/app/assets/stylesheets/command_proposal/terminal.scss +72 -0
- data/app/assets/stylesheets/command_proposal.scss +5 -0
- data/app/controllers/command_proposal/engine_controller.rb +6 -0
- data/app/controllers/command_proposal/iterations_controller.rb +83 -0
- data/app/controllers/command_proposal/runner_controller.rb +86 -0
- data/app/controllers/command_proposal/tasks_controller.rb +97 -0
- data/app/helpers/command_proposal/application_helper.rb +58 -0
- data/app/helpers/command_proposal/icons_helper.rb +15 -0
- data/app/helpers/command_proposal/params_helper.rb +63 -0
- data/app/helpers/command_proposal/permissions_helper.rb +42 -0
- data/app/jobs/command_proposal/application_job.rb +4 -0
- data/app/jobs/command_proposal/command_runner_job.rb +11 -0
- data/app/models/command_proposal/comment.rb +14 -0
- data/app/models/command_proposal/iteration.rb +78 -0
- data/app/models/command_proposal/service/external_belong.rb +48 -0
- data/app/models/command_proposal/service/json_wrapper.rb +18 -0
- data/app/models/command_proposal/service/proposal_presenter.rb +39 -0
- data/app/models/command_proposal/task.rb +106 -0
- data/app/views/command_proposal/tasks/_console_show.html.erb +44 -0
- data/app/views/command_proposal/tasks/_function_show.html.erb +54 -0
- data/app/views/command_proposal/tasks/_lines.html.erb +8 -0
- data/app/views/command_proposal/tasks/_module_show.html.erb +33 -0
- data/app/views/command_proposal/tasks/_past_iterations_list.html.erb +20 -0
- data/app/views/command_proposal/tasks/_task_detail_table.html.erb +31 -0
- data/app/views/command_proposal/tasks/_task_show.html.erb +55 -0
- data/app/views/command_proposal/tasks/error.html.erb +4 -0
- data/app/views/command_proposal/tasks/form.html.erb +64 -0
- data/app/views/command_proposal/tasks/index.html.erb +44 -0
- data/app/views/command_proposal/tasks/show.html.erb +10 -0
- data/config/routes.rb +11 -0
- data/lib/command_proposal/configuration.rb +41 -0
- data/lib/command_proposal/engine.rb +6 -0
- data/lib/command_proposal/services/command_interpreter.rb +108 -0
- data/lib/command_proposal/services/runner.rb +157 -0
- data/lib/command_proposal/version.rb +3 -0
- data/lib/command_proposal.rb +27 -0
- data/lib/generators/command_proposal/install/install_generator.rb +28 -0
- data/lib/generators/command_proposal/install/templates/initializer.rb +47 -0
- data/lib/generators/command_proposal/install/templates/install_command_proposal.rb +40 -0
- data/lib/tasks/command_proposal_tasks.rake +4 -0
- metadata +167 -0
@@ -0,0 +1 @@
|
|
1
|
+
@import "*";
|
@@ -0,0 +1,31 @@
|
|
1
|
+
.cmd-input {
|
2
|
+
margin: 3px;
|
3
|
+
border: 1px solid grey;
|
4
|
+
border-radius: 4px;
|
5
|
+
padding: 2px 4px;
|
6
|
+
width: 100%;
|
7
|
+
}
|
8
|
+
|
9
|
+
.cmd-flex-row {
|
10
|
+
display: flex;
|
11
|
+
}
|
12
|
+
|
13
|
+
.cmd-tab {
|
14
|
+
display: inline-block;
|
15
|
+
position: relative;
|
16
|
+
margin-right: -1px;
|
17
|
+
border: 1px solid grey;
|
18
|
+
border-radius: 6px 6px 0 0;
|
19
|
+
padding: 5px;
|
20
|
+
width: 100px;
|
21
|
+
color: inherit;
|
22
|
+
text-align: center;
|
23
|
+
text-decoration: none;
|
24
|
+
|
25
|
+
&.active {
|
26
|
+
border-bottom: none;
|
27
|
+
}
|
28
|
+
&:hover {
|
29
|
+
background: lightgrey;
|
30
|
+
}
|
31
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
.cmd-index-table {
|
2
|
+
.cmd-index-table-row {
|
3
|
+
text-decoration: none;
|
4
|
+
color: inherit;
|
5
|
+
display: flex;
|
6
|
+
width: 100%;
|
7
|
+
padding: 5px 0;
|
8
|
+
margin: 5px 0;
|
9
|
+
border-radius: 10px;
|
10
|
+
|
11
|
+
&:hover {
|
12
|
+
background: lightgrey;
|
13
|
+
}
|
14
|
+
.cmd-index-col {
|
15
|
+
&.cmd-col-status {
|
16
|
+
width: 100px;
|
17
|
+
}
|
18
|
+
&.cmd-col-title {
|
19
|
+
width: 100%;
|
20
|
+
}
|
21
|
+
&.cmd-col-type {
|
22
|
+
width: 120px;
|
23
|
+
}
|
24
|
+
&.cmd-col-info {
|
25
|
+
width: 200px;
|
26
|
+
padding-right: 10px;
|
27
|
+
}
|
28
|
+
|
29
|
+
.cmd-status {
|
30
|
+
text-align: center;
|
31
|
+
font-size: 20px;
|
32
|
+
}
|
33
|
+
.cmd-name {}
|
34
|
+
.cmd-duration {
|
35
|
+
font-size: 10px;
|
36
|
+
vertical-align: bottom;
|
37
|
+
}
|
38
|
+
.cmd-desc {
|
39
|
+
padding-left: 20px;
|
40
|
+
color: grey;
|
41
|
+
font-size: 12px;
|
42
|
+
}
|
43
|
+
.cmd-type {
|
44
|
+
color: grey;
|
45
|
+
font-size: 12px;
|
46
|
+
text-align: right;
|
47
|
+
}
|
48
|
+
.cmd-timestamp {
|
49
|
+
text-align: right;
|
50
|
+
}
|
51
|
+
.cmd-author {
|
52
|
+
text-align: right;
|
53
|
+
color: grey;
|
54
|
+
font-size: 12px;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
|
61
|
+
.cmd-table {
|
62
|
+
border-collapse: collapse;
|
63
|
+
table-layout: fixed;
|
64
|
+
width: 100%;
|
65
|
+
|
66
|
+
// Hack to stop the header from being highlighted, need high specificity.
|
67
|
+
thead tr:nth-child(1) th { background-color: transparent; }
|
68
|
+
th, td {
|
69
|
+
text-align: left;
|
70
|
+
padding: 3px;
|
71
|
+
}
|
72
|
+
th {
|
73
|
+
font-size: 12px;
|
74
|
+
color: grey;
|
75
|
+
}
|
76
|
+
}
|
@@ -0,0 +1,72 @@
|
|
1
|
+
.cmd-console {
|
2
|
+
margin: 5px 0;
|
3
|
+
font-size: 0;
|
4
|
+
display: block;
|
5
|
+
position: relative;
|
6
|
+
background: #112435;
|
7
|
+
padding: 10px 20px;
|
8
|
+
padding-left: 40px;
|
9
|
+
overflow-x: auto;
|
10
|
+
overflow-y: visible;
|
11
|
+
color: lime;
|
12
|
+
font-family: monospace;
|
13
|
+
text-align: left;
|
14
|
+
white-space: nowrap;
|
15
|
+
white-space: pre;
|
16
|
+
counter-reset: line-count;
|
17
|
+
|
18
|
+
.line {
|
19
|
+
min-height: 18px;
|
20
|
+
counter-increment: line-count;
|
21
|
+
font-size: 16px;
|
22
|
+
padding-left: 5px;
|
23
|
+
position: relative;
|
24
|
+
|
25
|
+
&:before {
|
26
|
+
content: "[" counter(line-count) "]>";
|
27
|
+
position: absolute;
|
28
|
+
left: -40px;
|
29
|
+
top: 3px;
|
30
|
+
width: 40px;
|
31
|
+
font-size: 10px;
|
32
|
+
vertical-align: middle;
|
33
|
+
text-align: right;
|
34
|
+
}
|
35
|
+
|
36
|
+
&:focus {
|
37
|
+
outline: none !important;
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
.result {
|
42
|
+
position: relative;
|
43
|
+
|
44
|
+
&:before {
|
45
|
+
content: "=>";
|
46
|
+
position: absolute;
|
47
|
+
left: -40px;
|
48
|
+
top: 3px;
|
49
|
+
width: 35px;
|
50
|
+
font-size: 10px;
|
51
|
+
vertical-align: middle;
|
52
|
+
text-align: right;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
.cmd-error {
|
57
|
+
color: red;
|
58
|
+
|
59
|
+
&:before {
|
60
|
+
content: "X";
|
61
|
+
color: red;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
*::selection {
|
66
|
+
background: lime;
|
67
|
+
color: black;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
.cmd-terminal {
|
71
|
+
white-space: pre-wrap;
|
72
|
+
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require_dependency "command_proposal/engine_controller"
|
2
|
+
require "command_proposal/services/command_interpreter"
|
3
|
+
|
4
|
+
class ::CommandProposal::IterationsController < ::CommandProposal::EngineController
|
5
|
+
include ::CommandProposal::ParamsHelper
|
6
|
+
helper ::CommandProposal::ParamsHelper
|
7
|
+
include ::CommandProposal::PermissionsHelper
|
8
|
+
helper ::CommandProposal::PermissionsHelper
|
9
|
+
|
10
|
+
layout "application"
|
11
|
+
|
12
|
+
def create
|
13
|
+
return error!("You do not have permission to run commands.") unless can_command?
|
14
|
+
|
15
|
+
@task = ::CommandProposal::Task.find_by!(friendly_id: params[:task_id])
|
16
|
+
# Should rescue/catch and render json
|
17
|
+
return error!("Can only run commands on type: :console") unless @task.console?
|
18
|
+
return error!("Session has not been approved.") unless has_approval?(@task)
|
19
|
+
|
20
|
+
if @task.iterations.many?
|
21
|
+
runner = ::CommandProposal.sessions["task:#{@task.id}"]
|
22
|
+
elsif @task.iterations.one?
|
23
|
+
# Track console details in first iteration
|
24
|
+
@task.first_iteration.update(started_at: Time.current, status: :started)
|
25
|
+
runner = ::CommandProposal::Services::Runner.new
|
26
|
+
::CommandProposal.sessions["task:#{@task.id}"] = runner
|
27
|
+
end
|
28
|
+
|
29
|
+
return error!("Session has expired. Please start a new session.") if runner.nil?
|
30
|
+
|
31
|
+
|
32
|
+
@task.user = command_user # Separate from update to ensure it's set first
|
33
|
+
@task.update(code: params[:code]) # Creates a new iteration
|
34
|
+
@iteration = @task.current_iteration
|
35
|
+
@iteration.update(status: :approved) # Task was already approved, and this is line-by-line
|
36
|
+
|
37
|
+
# in-sync
|
38
|
+
runner.execute(@iteration)
|
39
|
+
|
40
|
+
render json: {
|
41
|
+
result: @iteration.result
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def update
|
46
|
+
@iteration = ::CommandProposal::Iteration.find(params[:id])
|
47
|
+
|
48
|
+
begin
|
49
|
+
alter_command if params.dig(:command_proposal_iteration, :command).present?
|
50
|
+
rescue ::CommandProposal::Services::CommandInterpreter::Error => e
|
51
|
+
return redirect_to cmd_path(:tasks, :error), alert: e.message
|
52
|
+
end
|
53
|
+
|
54
|
+
sleep 0.2
|
55
|
+
|
56
|
+
redirect_to cmd_path(@iteration.task)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def iteration_params
|
62
|
+
{}.tap do |whitelist|
|
63
|
+
if params.dig(:command_proposal_iteration, :args).present?
|
64
|
+
whitelist[:args] = params.dig(:command_proposal_iteration, :args).permit!.to_h
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def alter_command
|
70
|
+
::CommandProposal::Services::CommandInterpreter.command(
|
71
|
+
@iteration,
|
72
|
+
params.dig(:command_proposal_iteration, :command),
|
73
|
+
command_user,
|
74
|
+
iteration_params
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def error!(msg="An error occurred.")
|
79
|
+
render json: {
|
80
|
+
error: msg
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_dependency "command_proposal/engine_controller"
|
2
|
+
require "command_proposal/services/command_interpreter"
|
3
|
+
|
4
|
+
class ::CommandProposal::RunnerController < ::CommandProposal::EngineController
|
5
|
+
include ::CommandProposal::ParamsHelper
|
6
|
+
include ::CommandProposal::PermissionsHelper
|
7
|
+
|
8
|
+
begin
|
9
|
+
# Skip load if app attempts to do that
|
10
|
+
skip_load_and_authorize_resource
|
11
|
+
rescue NameError => e
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
skip_before_action :verify_authenticity_token
|
16
|
+
before_action :authorize_command!, except: :error
|
17
|
+
|
18
|
+
def show
|
19
|
+
@task = ::CommandProposal::Task.find_by!(friendly_id: params[:task_id])
|
20
|
+
@iteration = ::CommandProposal::Iteration.find(params[:id])
|
21
|
+
|
22
|
+
iteration_response
|
23
|
+
end
|
24
|
+
|
25
|
+
def create
|
26
|
+
@task = ::CommandProposal::Task.find_by!(friendly_id: params[:task_id])
|
27
|
+
# Error unless @task.function?
|
28
|
+
# Error unless iteration is ready to be run
|
29
|
+
@iteration = @task.current_iteration
|
30
|
+
|
31
|
+
begin
|
32
|
+
@iteration = run_iteration
|
33
|
+
rescue ::CommandProposal::Services::CommandInterpreter::Error => e
|
34
|
+
return render(text: e.message, status: :bad_request)
|
35
|
+
end
|
36
|
+
|
37
|
+
iteration_response
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def run_iteration
|
43
|
+
::CommandProposal::Services::CommandInterpreter.command(
|
44
|
+
@iteration,
|
45
|
+
:run,
|
46
|
+
command_user,
|
47
|
+
iteration_params
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def iteration_params
|
52
|
+
{ args: params.permit!.to_unsafe_h.except(:action, :controller, :task_id, :runner) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def iteration_response
|
56
|
+
@iteration.reload
|
57
|
+
|
58
|
+
respond_to do |format|
|
59
|
+
format.json { render(json: iteration_json, status: :ok) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def iteration_json(opts={})
|
64
|
+
{
|
65
|
+
result: @iteration.result,
|
66
|
+
status: @iteration.status,
|
67
|
+
duration: humanized_duration(@iteration.duration),
|
68
|
+
}.tap do |response|
|
69
|
+
if @iteration.started?
|
70
|
+
response[:endpoint] = runner_path(@task, @iteration)
|
71
|
+
end
|
72
|
+
if @task.console?
|
73
|
+
response[:result_html] = ApplicationController.render(
|
74
|
+
partial: "command_proposal/tasks/lines",
|
75
|
+
locals: { lines: @task.lines }
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def authorize_command!
|
82
|
+
return if can_command?
|
83
|
+
|
84
|
+
render text: "Sorry, you are not authorized to do that."
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require_dependency "command_proposal/engine_controller"
|
2
|
+
|
3
|
+
class ::CommandProposal::TasksController < ::CommandProposal::EngineController
|
4
|
+
include ::CommandProposal::ParamsHelper
|
5
|
+
helper ::CommandProposal::ParamsHelper
|
6
|
+
include ::CommandProposal::PermissionsHelper
|
7
|
+
helper ::CommandProposal::PermissionsHelper
|
8
|
+
helper ::CommandProposal::IconsHelper
|
9
|
+
|
10
|
+
before_action :authorize_command!, except: :error
|
11
|
+
|
12
|
+
def search
|
13
|
+
redirect_to cmd_path(:tasks, current_params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def index
|
17
|
+
@tasks = ::CommandProposal::Task.includes(:iterations)
|
18
|
+
@tasks = @tasks.order(Arel.sql("COALESCE(command_proposal_tasks.last_executed_at, command_proposal_tasks.created_at) DESC"))
|
19
|
+
@tasks = @tasks.search(params[:search]) if params[:search].present?
|
20
|
+
@tasks = @tasks.where(session_type: params[:filter]) if params[:filter].present?
|
21
|
+
end
|
22
|
+
|
23
|
+
def show
|
24
|
+
@task = ::CommandProposal::Task.find_by!(friendly_id: params[:id])
|
25
|
+
if @task.console?
|
26
|
+
@lines = @task.lines
|
27
|
+
@iteration = @task.first_iteration
|
28
|
+
|
29
|
+
return # Don't execute the rest of the function
|
30
|
+
end
|
31
|
+
|
32
|
+
if params.key?(:iteration)
|
33
|
+
@iteration = @task.iterations.find(params[:iteration])
|
34
|
+
else
|
35
|
+
@iteration = @task.current_iteration
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def new
|
40
|
+
@task = ::CommandProposal::Task.new(session_type: params[:session_type])
|
41
|
+
|
42
|
+
render "form"
|
43
|
+
end
|
44
|
+
|
45
|
+
def edit
|
46
|
+
@task = ::CommandProposal::Task.find_by!(friendly_id: params[:id])
|
47
|
+
|
48
|
+
render "form"
|
49
|
+
end
|
50
|
+
|
51
|
+
def create
|
52
|
+
@task = ::CommandProposal::Task.new(task_params.except(:code))
|
53
|
+
@task.user = command_user
|
54
|
+
|
55
|
+
# Cannot create the iteration until the task is created, so save then update
|
56
|
+
if @task.save && @task.update(task_params)
|
57
|
+
if @task.console?
|
58
|
+
@task.iterations.create(requester: command_user) # Blank iteration to track approval
|
59
|
+
redirect_to cmd_path(@task)
|
60
|
+
else
|
61
|
+
redirect_to cmd_path(:edit, @task)
|
62
|
+
end
|
63
|
+
else
|
64
|
+
# TODO: Display errors
|
65
|
+
render "form"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def update
|
70
|
+
@task = ::CommandProposal::Task.find_by!(friendly_id: params[:id])
|
71
|
+
@task.user = command_user
|
72
|
+
|
73
|
+
if @task.update(task_params)
|
74
|
+
redirect_to cmd_path(@task)
|
75
|
+
else
|
76
|
+
# TODO: Display errors
|
77
|
+
render "form"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def task_params
|
84
|
+
params.require(:command_proposal_task).permit(
|
85
|
+
:name,
|
86
|
+
:description,
|
87
|
+
:session_type,
|
88
|
+
:code,
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def authorize_command!
|
93
|
+
return if can_command?
|
94
|
+
|
95
|
+
redirect_to cmd_path(:error, :tasks), alert: "Sorry, you are not authorized to access this page."
|
96
|
+
end
|
97
|
+
end
|