jobs_dashboard 0.3.4

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