command_proposal 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +77 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/command_proposal_manifest.js +1 -0
  6. data/app/assets/javascripts/command_proposal/_codemirror.js +9814 -0
  7. data/app/assets/javascripts/command_proposal/_helpers.js +9 -0
  8. data/app/assets/javascripts/command_proposal/codemirror-addon-searchcursor.js +296 -0
  9. data/app/assets/javascripts/command_proposal/codemirror-keymap-sublime.js +720 -0
  10. data/app/assets/javascripts/command_proposal/codemirror-mode-ruby.js +303 -0
  11. data/app/assets/javascripts/command_proposal/console.js +195 -0
  12. data/app/assets/javascripts/command_proposal/feed.js +51 -0
  13. data/app/assets/javascripts/command_proposal/terminal.js +40 -0
  14. data/app/assets/javascripts/command_proposal.js +1 -0
  15. data/app/assets/stylesheets/command_proposal/_variables.scss +0 -0
  16. data/app/assets/stylesheets/command_proposal/codemirror-rubyblue.scss +27 -0
  17. data/app/assets/stylesheets/command_proposal/codemirror.scss +367 -0
  18. data/app/assets/stylesheets/command_proposal/command_proposal.scss +1 -0
  19. data/app/assets/stylesheets/command_proposal/components.scss +31 -0
  20. data/app/assets/stylesheets/command_proposal/containers.scss +4 -0
  21. data/app/assets/stylesheets/command_proposal/icons.scss +12 -0
  22. data/app/assets/stylesheets/command_proposal/tables.scss +76 -0
  23. data/app/assets/stylesheets/command_proposal/terminal.scss +72 -0
  24. data/app/assets/stylesheets/command_proposal.scss +5 -0
  25. data/app/controllers/command_proposal/engine_controller.rb +6 -0
  26. data/app/controllers/command_proposal/iterations_controller.rb +83 -0
  27. data/app/controllers/command_proposal/runner_controller.rb +86 -0
  28. data/app/controllers/command_proposal/tasks_controller.rb +97 -0
  29. data/app/helpers/command_proposal/application_helper.rb +58 -0
  30. data/app/helpers/command_proposal/icons_helper.rb +15 -0
  31. data/app/helpers/command_proposal/params_helper.rb +63 -0
  32. data/app/helpers/command_proposal/permissions_helper.rb +42 -0
  33. data/app/jobs/command_proposal/application_job.rb +4 -0
  34. data/app/jobs/command_proposal/command_runner_job.rb +11 -0
  35. data/app/models/command_proposal/comment.rb +14 -0
  36. data/app/models/command_proposal/iteration.rb +78 -0
  37. data/app/models/command_proposal/service/external_belong.rb +48 -0
  38. data/app/models/command_proposal/service/json_wrapper.rb +18 -0
  39. data/app/models/command_proposal/service/proposal_presenter.rb +39 -0
  40. data/app/models/command_proposal/task.rb +106 -0
  41. data/app/views/command_proposal/tasks/_console_show.html.erb +44 -0
  42. data/app/views/command_proposal/tasks/_function_show.html.erb +54 -0
  43. data/app/views/command_proposal/tasks/_lines.html.erb +8 -0
  44. data/app/views/command_proposal/tasks/_module_show.html.erb +33 -0
  45. data/app/views/command_proposal/tasks/_past_iterations_list.html.erb +20 -0
  46. data/app/views/command_proposal/tasks/_task_detail_table.html.erb +31 -0
  47. data/app/views/command_proposal/tasks/_task_show.html.erb +55 -0
  48. data/app/views/command_proposal/tasks/error.html.erb +4 -0
  49. data/app/views/command_proposal/tasks/form.html.erb +64 -0
  50. data/app/views/command_proposal/tasks/index.html.erb +44 -0
  51. data/app/views/command_proposal/tasks/show.html.erb +10 -0
  52. data/config/routes.rb +11 -0
  53. data/lib/command_proposal/configuration.rb +41 -0
  54. data/lib/command_proposal/engine.rb +6 -0
  55. data/lib/command_proposal/services/command_interpreter.rb +108 -0
  56. data/lib/command_proposal/services/runner.rb +157 -0
  57. data/lib/command_proposal/version.rb +3 -0
  58. data/lib/command_proposal.rb +27 -0
  59. data/lib/generators/command_proposal/install/install_generator.rb +28 -0
  60. data/lib/generators/command_proposal/install/templates/initializer.rb +47 -0
  61. data/lib/generators/command_proposal/install/templates/install_command_proposal.rb +40 -0
  62. data/lib/tasks/command_proposal_tasks.rake +4 -0
  63. metadata +167 -0
@@ -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,4 @@
1
+ .cmd-wrapper {
2
+ max-width: 1200px;
3
+ margin: 0 auto;
4
+ }
@@ -0,0 +1,12 @@
1
+ .cmd-icon-grey {
2
+ color: grey;
3
+ }
4
+ .cmd-icon-green {
5
+ color: green;
6
+ }
7
+ .cmd-icon-yellow {
8
+ color: orange;
9
+ }
10
+ .cmd-icon-red {
11
+ color: red;
12
+ }
@@ -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,5 @@
1
+ /*
2
+ *= require font-awesome
3
+ *= require_tree .
4
+ *= require_self
5
+ */
@@ -0,0 +1,6 @@
1
+ module CommandProposal
2
+ class EngineController < ::ApplicationController
3
+ include ::CommandProposal::ApplicationHelper
4
+ helper Rails.application.routes.url_helpers
5
+ end
6
+ end
@@ -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