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.
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