command_proposal 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|