jobs_dashboard 0.3.4

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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gem-push.yml +34 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/CHANGELOG.md +60 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.lock +129 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +133 -0
  10. data/Rakefile +8 -0
  11. data/app/assets/stylesheets/jobs_dashboard/application.scss +214 -0
  12. data/app/assets/stylesheets/jobs_dashboard/modules/buttons.sass +15 -0
  13. data/app/assets/stylesheets/jobs_dashboard/modules/card.sass +22 -0
  14. data/app/assets/stylesheets/jobs_dashboard/modules/description.sass +17 -0
  15. data/app/assets/stylesheets/jobs_dashboard/modules/grid.sass +17 -0
  16. data/app/assets/stylesheets/jobs_dashboard/modules/inputs.sass +31 -0
  17. data/app/assets/stylesheets/jobs_dashboard/modules/item-list.sass +10 -0
  18. data/app/assets/stylesheets/jobs_dashboard/modules/labels.sass +12 -0
  19. data/app/assets/stylesheets/jobs_dashboard/modules/statistics.sass +15 -0
  20. data/app/assets/stylesheets/jobs_dashboard/modules/table.sass +38 -0
  21. data/app/assets/stylesheets/jobs_dashboard/pages/jobs_logs/show.sass +36 -0
  22. data/app/assets/stylesheets/jobs_dashboard/reset.scss +135 -0
  23. data/app/assets/stylesheets/jobs_dashboard/variables.sass +6 -0
  24. data/app/controllers/jobs_dashboard/application_controller.rb +20 -0
  25. data/app/controllers/jobs_dashboard/dashboard_controller.rb +12 -0
  26. data/app/controllers/jobs_dashboard/job_logs_controller.rb +17 -0
  27. data/app/helpers/jobs_dashboard/application_helper.rb +31 -0
  28. data/app/models/jobs_dashboard/application_record.rb +5 -0
  29. data/app/models/jobs_dashboard/job_log.rb +32 -0
  30. data/app/views/jobs_dashboard/dashboard/_job_row.slim +8 -0
  31. data/app/views/jobs_dashboard/dashboard/index.slim +40 -0
  32. data/app/views/jobs_dashboard/job_logs/_stats.html.erb +0 -0
  33. data/app/views/jobs_dashboard/job_logs/index.slim +84 -0
  34. data/app/views/jobs_dashboard/job_logs/show.slim +89 -0
  35. data/app/views/kaminari/jobs_dashboard/_first_page.html.erb +11 -0
  36. data/app/views/kaminari/jobs_dashboard/_gap.html.erb +8 -0
  37. data/app/views/kaminari/jobs_dashboard/_last_page.html.erb +11 -0
  38. data/app/views/kaminari/jobs_dashboard/_next_page.html.erb +11 -0
  39. data/app/views/kaminari/jobs_dashboard/_page.html.erb +12 -0
  40. data/app/views/kaminari/jobs_dashboard/_paginator.html.erb +25 -0
  41. data/app/views/kaminari/jobs_dashboard/_prev_page.html.erb +11 -0
  42. data/app/views/layouts/jobs_dashboard/application.html.erb +15 -0
  43. data/app/views/shared/_logo.slim +4 -0
  44. data/app/views/shared/_navigation.slim +12 -0
  45. data/bin/console +15 -0
  46. data/bin/setup +8 -0
  47. data/config/initializers/ransack.rb +11 -0
  48. data/config/locales/default.fr.yml +219 -0
  49. data/config/locales/jobs_dashboard.en.yml +33 -0
  50. data/config/locales/jobs_dashboard.fr.yml +37 -0
  51. data/config/routes.rb +5 -0
  52. data/jobs_dashboard.gemspec +38 -0
  53. data/lib/generators/active_record/job_log_migration_generator.rb +24 -0
  54. data/lib/generators/active_record/templates/migration.rb +20 -0
  55. data/lib/jobs_dashboard/client_middleware.rb +56 -0
  56. data/lib/jobs_dashboard/engine.rb +18 -0
  57. data/lib/jobs_dashboard/server_middleware.rb +62 -0
  58. data/lib/jobs_dashboard/storage.rb +54 -0
  59. data/lib/jobs_dashboard/version.rb +5 -0
  60. data/lib/jobs_dashboard/worker.rb +25 -0
  61. data/lib/jobs_dashboard.rb +12 -0
  62. metadata +161 -0
@@ -0,0 +1,17 @@
1
+ @import "../variables"
2
+
3
+ .row
4
+ display: flex
5
+ margin:
6
+ left: -16px
7
+ right: -16px
8
+
9
+ div[class^='col-']
10
+ display: flex
11
+ padding: 0 16px
12
+
13
+ .col-12
14
+ width: 50%
15
+
16
+ .col-24
17
+ width: 100%
@@ -0,0 +1,31 @@
1
+ .form-inline
2
+ display: flex
3
+ gap: 16px
4
+ flex-wrap: wrap
5
+ align-items: center
6
+ &.submit
7
+ margin-top: 1rem
8
+ justify-content: flex-end
9
+ align-items: baseline
10
+
11
+ > .input
12
+ label
13
+ display: block
14
+ font-weight: 600
15
+ line-height: 2rem
16
+ color: #343434
17
+ input, select
18
+ min-width: 150px
19
+ &.group
20
+ display: flex
21
+ flex-direction: column
22
+
23
+ input, select
24
+ border-radius: 0.375rem
25
+ padding: 0px 12px
26
+ border: 1px solid #6b7280
27
+ box-shadow: none
28
+ font-size: 1rem
29
+ line-height: 1.5rem
30
+ height: 37px
31
+ outline-color: #A63446
@@ -0,0 +1,10 @@
1
+ @import "../variables"
2
+
3
+ ul.item-list
4
+ list-style: none
5
+ padding: 0
6
+ margin: 0
7
+ > li
8
+ padding: 8px 0
9
+ > li:not(:last-child)
10
+ border-bottom: 1px solid $border-color
@@ -0,0 +1,12 @@
1
+ .status-label
2
+ width: 10px
3
+ height: 10px
4
+ display: inline-block
5
+ border-radius: 99px
6
+ margin-right: 5px
7
+ background: #3F84E5
8
+ &.success
9
+ background: #2ecc71
10
+ &.error
11
+ background: #e74c3c
12
+
@@ -0,0 +1,15 @@
1
+ @import "../variables"
2
+ .statistics
3
+ display: flex
4
+ justify-content: space-around
5
+ .stat
6
+ text-align: center
7
+ strong
8
+ font-size: 24px
9
+ margin-bottom: 1rem
10
+ justify-content: center
11
+ display: flex
12
+ color: $primary-color
13
+ font-weight: 600
14
+ font-size: 14px
15
+ color: $jet
@@ -0,0 +1,38 @@
1
+ $header-color: #f5f5f5
2
+ $table-radius: 6px
3
+ $cell-padding: 14px 10px
4
+ $table-border-color: #e4e4e4
5
+
6
+ table
7
+ border-radius: $table-radius
8
+ border: 1px solid $table-border-color
9
+ border-spacing: 0
10
+ border-collapse: separate
11
+
12
+ > thead
13
+ > tr
14
+ > th
15
+ border: 0
16
+ border-bottom: 1px solid $table-border-color
17
+ font-weight: 600
18
+ background-color: $header-color
19
+ color: darken($header-color, 40%)
20
+ padding: $cell-padding
21
+
22
+ &:first-child
23
+ border-top-left-radius: $table-radius
24
+ &:last-child
25
+ border-top-right-radius: $table-radius
26
+ > tbody
27
+ > tr
28
+ > td
29
+ border: 0
30
+ border-bottom: 1px solid $table-border-color
31
+ padding: $cell-padding
32
+ &:first-child
33
+ border-bottom-left-radius: $table-radius
34
+ &:last-child
35
+ border-bottom-right-radius: $table-radius
36
+
37
+ table tbody tr:hover
38
+ background-color: $header-color
@@ -0,0 +1,36 @@
1
+ .job_logs-show
2
+ .page-container
3
+ display: flex
4
+ flex-direction: column
5
+ width: 100%
6
+
7
+ .page-header
8
+ display: flex
9
+ flex-direction: column
10
+ gap: 20px
11
+
12
+ .title
13
+ font-size: 18px
14
+ font-weight: 600
15
+
16
+ .page-body
17
+ display: flex
18
+ flex-direction: row
19
+ gap: 20px
20
+
21
+ .main-container
22
+ flex: 8
23
+ display: flex
24
+ flex-direction: column
25
+ gap: 20px
26
+
27
+ .side-container
28
+ flex: 3
29
+
30
+
31
+ .no-padding
32
+ padding: 0 !important
33
+
34
+ .card-no-padding-title
35
+ padding: 18px 30px
36
+
@@ -0,0 +1,135 @@
1
+ /* http://meyerweb.com/eric/tools/css/reset/
2
+ v2.0 | 20110126
3
+ License: none (public domain)
4
+ */
5
+
6
+ html,
7
+ body,
8
+ div,
9
+ span,
10
+ applet,
11
+ object,
12
+ iframe,
13
+ h1,
14
+ h2,
15
+ h3,
16
+ h4,
17
+ h5,
18
+ h6,
19
+ p,
20
+ blockquote,
21
+ pre,
22
+ a,
23
+ abbr,
24
+ acronym,
25
+ address,
26
+ big,
27
+ cite,
28
+ code,
29
+ del,
30
+ dfn,
31
+ em,
32
+ img,
33
+ ins,
34
+ kbd,
35
+ q,
36
+ s,
37
+ samp,
38
+ small,
39
+ strike,
40
+ strong,
41
+ sub,
42
+ sup,
43
+ tt,
44
+ var,
45
+ b,
46
+ u,
47
+ i,
48
+ center,
49
+ dl,
50
+ dt,
51
+ dd,
52
+ ol,
53
+ ul,
54
+ li,
55
+ fieldset,
56
+ form,
57
+ label,
58
+ legend,
59
+ table,
60
+ caption,
61
+ tbody,
62
+ tfoot,
63
+ thead,
64
+ tr,
65
+ th,
66
+ td,
67
+ article,
68
+ aside,
69
+ canvas,
70
+ details,
71
+ embed,
72
+ figure,
73
+ figcaption,
74
+ footer,
75
+ header,
76
+ hgroup,
77
+ menu,
78
+ nav,
79
+ output,
80
+ ruby,
81
+ section,
82
+ summary,
83
+ time,
84
+ mark,
85
+ audio,
86
+ video {
87
+ margin: 0;
88
+ padding: 0;
89
+ border: 0;
90
+ font-size: 100%;
91
+ font: inherit;
92
+ vertical-align: baseline;
93
+ }
94
+
95
+ /* HTML5 display-role reset for older browsers */
96
+ article,
97
+ aside,
98
+ details,
99
+ figcaption,
100
+ figure,
101
+ footer,
102
+ header,
103
+ hgroup,
104
+ menu,
105
+ nav,
106
+ section {
107
+ display: block;
108
+ }
109
+
110
+ body {
111
+ line-height: 1;
112
+ }
113
+
114
+ ol,
115
+ ul {
116
+ list-style: none;
117
+ }
118
+
119
+ blockquote,
120
+ q {
121
+ quotes: none;
122
+ }
123
+
124
+ blockquote:before,
125
+ blockquote:after,
126
+ q:before,
127
+ q:after {
128
+ content: '';
129
+ content: none;
130
+ }
131
+
132
+ table {
133
+ border-collapse: collapse;
134
+ border-spacing: 0;
135
+ }
@@ -0,0 +1,6 @@
1
+ $primary-color: #A63446
2
+ $montserrat: 'Montserrat', sans-serif
3
+ $jet: #2D4047
4
+ $muted: #696969
5
+
6
+ $border-color: #E6E6E6
@@ -0,0 +1,20 @@
1
+ require 'kaminari'
2
+ module JobsDashboard
3
+ class ApplicationController < ActionController::Base
4
+ before_action :basic_auth
5
+ before_action :set_locale
6
+
7
+ private
8
+
9
+ def basic_auth
10
+ if ENV["JOBS_DASHBOARD_AUTH_USERNAME"].blank? || ENV["JOBS_DASHBOARD_AUTH_PASSWORD"].blank?
11
+ raise 'Define a JOBS_DASHBOARD_AUTH_USERNAME and JOBS_DASHBOARD_AUTH_PASSWORD to access jobs dashboard'
12
+ end
13
+ self.class.http_basic_authenticate_with name: ENV["JOBS_DASHBOARD_AUTH_USERNAME"], password: ENV["JOBS_DASHBOARD_AUTH_PASSWORD"]
14
+ end
15
+
16
+ def set_locale
17
+ I18n.locale = :fr
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module JobsDashboard
2
+ class DashboardController < ApplicationController
3
+
4
+ def index
5
+ @stats = JobLog.all.select("COUNT(id) as count, status").group(:status)
6
+ @awaiting_jobs = JobLog.queued
7
+ @ongoing_jobs = JobLog.working
8
+ end
9
+
10
+ end
11
+
12
+ end
@@ -0,0 +1,17 @@
1
+ module JobsDashboard
2
+ class JobLogsController < ApplicationController
3
+ def index
4
+ @q = JobLog.ransack(params[:q])
5
+
6
+ @job_logs = @q.result(distinct: true).order(created_at: :desc)
7
+ if @job_logs.respond_to?(:page)
8
+ @job_logs = @job_logs.page(params[:page]).per(100)
9
+ end
10
+ end
11
+
12
+ def show
13
+ @job_log = JobLog.find_by(sidekiq_jid: params[:id])
14
+ @title = "#{@job_log.item_type}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module JobsDashboard
3
+ module ApplicationHelper
4
+ def display_time time
5
+ time.in_time_zone(DEFAULT_LOCAL_TIME_ZONE).strftime("%Y-%m-%d %I:%M:%S")
6
+ end
7
+
8
+ def status_label status
9
+ case status
10
+ when 'complete'
11
+ return 'success'
12
+ when 'failed', 'interrupted'
13
+ return 'error'
14
+ end
15
+ end
16
+
17
+ def item_types_collection
18
+ JobLog.pluck(:item_type).compact.uniq.sort
19
+ end
20
+
21
+ def statuses_collection
22
+ JobLog.statuses.keys.map do |key|
23
+ [I18n.t("jobs_dashboard.statuses.#{key}"), key]
24
+ end
25
+ end
26
+
27
+ def queues_collection
28
+ JobLog.pluck(:queue).compact.uniq.sort
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ module JobsDashboard
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,32 @@
1
+ module JobsDashboard
2
+ DEFAULT_LOCAL_TIME_ZONE = 'Europe/Paris'
3
+
4
+ class JobLog < ApplicationRecord
5
+ enum status: {
6
+ queued: 'queued',
7
+ working: 'working',
8
+ retrying: 'retrying',
9
+ complete: 'complete',
10
+ failed: 'failed',
11
+ interrupted: 'interrupted'
12
+ }
13
+
14
+ serialize :args, Array
15
+ serialize :metadata, Hash
16
+ serialize :logs, Array
17
+ serialize :backtrace, Array
18
+
19
+ validates :sidekiq_jid, presence: true, uniqueness: true
20
+
21
+ def self.ransackable_attributes(auth_object = nil)
22
+ [
23
+ "sidekiq_jid",
24
+ "item_type",
25
+ "queue",
26
+ "status",
27
+ "created_at",
28
+ "finished_at",
29
+ ]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ li
2
+ div.d-flex
3
+ = link_to job.sidekiq_jid, job_log_path(job.sidekiq_jid)
4
+ small.ml-auto
5
+ = l job.created_at, format: :short
6
+
7
+ div
8
+ small.text-muted= job.item_type
@@ -0,0 +1,40 @@
1
+ .row
2
+ .col-24
3
+ .card.shadowed.full-width
4
+ .card-content
5
+ .statistics
6
+ - [:working, :queued, :complete, :failed].each do |state|
7
+ .stat
8
+ strong
9
+ = @stats.find{|stat| stat[:status] === state.to_s }.try(:[], :count) || 0
10
+ = t state, scope: :'jobs_dashboard.statuses'
11
+
12
+ .row.mt-1
13
+ .col-12
14
+ .card.shadowed.full-width
15
+ .card-title
16
+ => t :'jobs_dashboard.statuses.working'
17
+ - if @ongoing_jobs.count > 0
18
+ |(#{@ongoing_jobs.count})
19
+ .card-content
20
+ - if @ongoing_jobs.any?
21
+ ul.item-list
22
+ - @ongoing_jobs.limit(10).each do |job|
23
+ = render partial: 'job_row', locals: { job: job }
24
+ - else
25
+ small.text-muted
26
+ = t :'jobs_dashboard.sentences.no_ongoing_jobs'
27
+ .col-12
28
+ .card.shadowed.full-width
29
+ .card-title
30
+ => t :'jobs_dashboard.statuses.queued'
31
+ - if @awaiting_jobs.count > 0
32
+ |(#{@awaiting_jobs.count})
33
+ .card-content
34
+ - if @awaiting_jobs.any?
35
+ ul.item-list
36
+ - @awaiting_jobs.limit(10).each do |job|
37
+ = render partial: 'job_row', locals: { job: job }
38
+ - else
39
+ small.text-muted
40
+ = t :'jobs_dashboard.sentences.no_awaiting_jobs'
File without changes
@@ -0,0 +1,84 @@
1
+ .card
2
+ .card-content
3
+ div
4
+ = search_form_for @q do |f|
5
+ .form-inline
6
+ .input
7
+ = f.label :sidekiq_jid_eq, t(:sidekiq_jid, scope: 'jobs_dashboard.table.headers')
8
+ = f.search_field :sidekiq_jid_eq
9
+ .input
10
+ = f.label :item_type_eq, t(:item_type, scope: 'jobs_dashboard.table.headers')
11
+ = f.select :item_type_eq,
12
+ options_for_select(item_types_collection,
13
+ params.dig(:q, :item_type_eq)),
14
+ { include_blank: t('form.select.all') },
15
+ { multiple: false }
16
+ .input
17
+ = f.label :status_eq, t(:status, scope: 'jobs_dashboard.table.headers')
18
+ = f.select :status_eq,
19
+ options_for_select(statuses_collection,
20
+ params.dig(:q, :status_eq)),
21
+ { include_blank: t('form.select.all') },
22
+ { multiple: false }
23
+ .input
24
+ = f.label :queue_eq, t(:queue, scope: 'jobs_dashboard.table.headers')
25
+ = f.select :queue_eq,
26
+ options_for_select(queues_collection,
27
+ params.dig(:q, :queue_eq)),
28
+ { include_blank: t('form.select.all') },
29
+ { multiple: false }
30
+ .input.group
31
+ = f.label :created_at_dategteq, t(:created_at, scope: 'jobs_dashboard.table.headers')
32
+ .d-flex.align-items-center
33
+ = f.date_field :created_at_dategteq
34
+ div style="margin-right: 5px; margin-left: 5px" = t('jobs_dashboard.words.to')
35
+ = f.date_field :created_at_datelteq
36
+
37
+ .input.group
38
+ = f.label :finished_at_dategteq, t(:finished_at, scope: 'jobs_dashboard.table.headers')
39
+ .d-flex.align-items-center
40
+ = f.date_field :finished_at_dategteq
41
+ div style="margin-right: 5px; margin-left: 5px" = t('jobs_dashboard.words.to')
42
+ = f.date_field :finished_at_datelteq
43
+ .form-inline.submit
44
+ = link_to t('jobs_dashboard.sentences.clear_filters'), job_logs_path
45
+ div style="margin-left: 30px"
46
+ = f.submit t('jobs_dashboard.words.submit'), class: 'button primary'
47
+ .row.mt-2
48
+ .col-24
49
+ table class="full-width"
50
+ thead
51
+ tr
52
+ th
53
+ = t(:sidekiq_jid, scope: 'jobs_dashboard.table.headers')
54
+ th
55
+ = t(:status, scope: 'jobs_dashboard.table.headers')
56
+ th
57
+ = t(:queue, scope: 'jobs_dashboard.table.headers')
58
+ th
59
+ = t(:item_type, scope: 'jobs_dashboard.table.headers')
60
+ - JobsDashboard.additional_params.each do |param|
61
+ th= param[:title]
62
+ th.text-right
63
+ = t(:created_at, scope: 'jobs_dashboard.table.headers')
64
+ th.text-right
65
+ = t(:finished_at, scope: 'jobs_dashboard.table.headers')
66
+ tbody
67
+ - @job_logs.each do |job_log|
68
+ tr
69
+ td= link_to job_log.sidekiq_jid, job_log_path(job_log.sidekiq_jid)
70
+ td
71
+ .status-label class=(status_label(job_log.status))
72
+ = t job_log.status, scope: 'jobs_dashboard.statuses'
73
+ td= job_log.queue
74
+ td= job_log.item_type
75
+ - JobsDashboard.additional_params.each do |param|
76
+ td
77
+ = job_log.try(:[], param[:attribute_name])
78
+ td.text-right
79
+ = display_time job_log.created_at
80
+ td.text-right
81
+ = job_log.finished_at.present? ? display_time(job_log.finished_at) : '-'
82
+
83
+ .pagination
84
+ = paginate @job_logs, theme: 'jobs_dashboard'
@@ -0,0 +1,89 @@
1
+ .page-container
2
+ .page-body
3
+ .main-container
4
+ - if @job_log.backtrace.present?
5
+ .card.shadowed
6
+ .card-title Error message
7
+ .card-content
8
+ code
9
+ = @job_log.error_message
10
+
11
+ .card.shadowed
12
+ .card-title Backtrace
13
+ .card-content
14
+ div.radius style="max-height: 400px; overflow-y: auto;"
15
+ .code
16
+ code
17
+ - @job_log.backtrace.each do |b|
18
+ div = b
19
+
20
+ - if @job_log.args.present?
21
+ .card.shadowed
22
+ .card-title Arguments
23
+ .card-content.no-padding
24
+ div.radius style="max-height: 400px; overflow-y: auto;"
25
+ .description
26
+ - @job_log.args.each do |arg|
27
+ - if arg.is_a? Hash
28
+ - arg.each do |k,v|
29
+ .item
30
+ .title
31
+ code = k
32
+ .value
33
+ code = v
34
+ - else
35
+ .item
36
+ .title
37
+ code = @job_log.args.index(arg)
38
+ .value
39
+ code = arg
40
+
41
+ - if @job_log.respond_to?(:metadata) && @job_log.metadata.present?
42
+ .card.shadowed.no-padding
43
+ .card-title Metadata
44
+ .card-content
45
+ div.radius style="max-height: 400px; overflow-y: auto;"
46
+ .description
47
+ - @job_log.metadata.each do |k, v|
48
+ .item
49
+ .title
50
+ code = k
51
+ .value
52
+ code = v
53
+
54
+ - if @job_log.respond_to?(:logs) && @job_log.logs.any?
55
+ .card.shadowed
56
+ .card-title Logs
57
+ .card-content.no-padding
58
+ div.radius style="max-height: 400px; overflow-y: auto;"
59
+ .description
60
+ - @job_log.logs.each do |log|
61
+ .item
62
+ .title = display_time(log[:created_at])
63
+ .value = log[:value]
64
+
65
+ .side-container
66
+ .card.shadowed
67
+ .card-title Infos
68
+ .card-content.no-padding
69
+ .description
70
+ .item
71
+ .title = t(:job_id, scope: 'jobs_dashboard.table.headers')
72
+ .value = @job_log.sidekiq_jid
73
+ .item
74
+ .title = t(:item_type, scope: 'jobs_dashboard.table.headers')
75
+ .value = @job_log.item_type
76
+ .item
77
+ .title = t(:created_at, scope: 'jobs_dashboard.table.headers')
78
+ .value = display_time(@job_log.created_at)
79
+ .item
80
+ .title = t(:finished_at, scope: 'jobs_dashboard.table.headers')
81
+ .value = @job_log.finished_at.present? ? display_time(@job_log.finished_at) : '-'
82
+ .item
83
+ .title = t(:status, scope: 'jobs_dashboard.table.headers')
84
+ .value
85
+ .status-label class=(status_label(@job_log.status))
86
+ = t @job_log.status, scope: 'jobs_dashboard.statuses'
87
+ .item
88
+ .title = t(:queue, scope: 'jobs_dashboard.table.headers')
89
+ .value = @job_log.queue
@@ -0,0 +1,11 @@
1
+ <%# Link to the "First" page
2
+ - available local variables
3
+ url: url to the first page
4
+ current_page: a page object for the currently displayed page
5
+ total_pages: total number of pages
6
+ per_page: number of items to fetch per page
7
+ remote: data-remote
8
+ -%>
9
+ <span class="first">
10
+ <%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %>
11
+ </span>
@@ -0,0 +1,8 @@
1
+ <%# Non-link tag that stands for skipped pages...
2
+ - available local variables
3
+ current_page: a page object for the currently displayed page
4
+ total_pages: total number of pages
5
+ per_page: number of items to fetch per page
6
+ remote: data-remote
7
+ -%>
8
+ <span class="page gap"><%= t('views.pagination.truncate').html_safe %></span>