solid_queue_dashboard 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/Procfile.dev +2 -1
  4. data/README.md +13 -9
  5. data/app/assets/javascripts/solid_queue_dashboard/application.js +17 -3
  6. data/app/assets/stylesheets/solid_queue_dashboard/application.css +96 -24
  7. data/app/assets/stylesheets/solid_queue_dashboard/tailwind.css +5 -5
  8. data/app/controllers/solid_queue_dashboard/appearance_controller.rb +1 -1
  9. data/app/controllers/solid_queue_dashboard/dashboard_controller.rb +61 -1
  10. data/app/controllers/solid_queue_dashboard/jobs_controller.rb +1 -1
  11. data/app/controllers/solid_queue_dashboard/stats_controller.rb +8 -0
  12. data/app/helpers/solid_queue_dashboard/icons_helper.rb +23 -0
  13. data/app/helpers/solid_queue_dashboard/jobs_helper.rb +8 -2
  14. data/app/helpers/solid_queue_dashboard/pagination_helper.rb +2 -2
  15. data/app/helpers/solid_queue_dashboard/processes_helper.rb +4 -2
  16. data/app/views/layouts/solid_queue_dashboard/application.html.erb +5 -3
  17. data/app/views/solid_queue_dashboard/application/_navbar.html.erb +28 -5
  18. data/app/views/solid_queue_dashboard/dashboard/index.html.erb +16 -28
  19. data/app/views/solid_queue_dashboard/jobs/_table.html.erb +1 -1
  20. data/app/views/solid_queue_dashboard/jobs/_table_row.html.erb +19 -3
  21. data/app/views/solid_queue_dashboard/jobs/index.html.erb +5 -5
  22. data/app/views/solid_queue_dashboard/jobs/show.html.erb +5 -3
  23. data/app/views/solid_queue_dashboard/processes/_table_row.html.erb +6 -0
  24. data/app/views/solid_queue_dashboard/processes/show.html.erb +14 -1
  25. data/app/views/solid_queue_dashboard/recurring_tasks/show.html.erb +6 -1
  26. data/app/views/solid_queue_dashboard/stats/index.html.erb +25 -0
  27. data/config/routes.rb +2 -0
  28. data/lib/solid_queue_dashboard/decorators/job_decorator.rb +9 -2
  29. data/lib/solid_queue_dashboard/decorators/jobs_decorator.rb +7 -0
  30. data/lib/solid_queue_dashboard/job.rb +5 -3
  31. data/lib/solid_queue_dashboard/process.rb +3 -3
  32. data/lib/solid_queue_dashboard/version.rb +1 -1
  33. data/lib/solid_queue_dashboard.rb +2 -0
  34. metadata +32 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bab2c28edcd8751ad636dcae7b2a464e937c4bf538c77a6f6be7f684bcc21e1d
4
- data.tar.gz: '0701197567cfa3ed1f5a54add9f6b7b59bec01ddaa014b6213885e22ef724774'
3
+ metadata.gz: a6d60665de93e100be378a8e607aaf251e62bf7c991051633dd1aba8d9b2a84f
4
+ data.tar.gz: 47a0d627dbf19a6be53260c846ca3baa35038f64bb7d6e844d77a1de68e62c83
5
5
  SHA512:
6
- metadata.gz: 5f40078988481a4f51e3433de13ce9640d218dda38ece0179e6d0b2526d78014db593b62c7b5ee69a4fa1c17c95a933da107c95690b550927ee4508d633595ed
7
- data.tar.gz: 3a731f4e5c33e77c5989c5f290d7befba922aefd46c0febf4d8de86ad1e6b261825b147d8996394556f43d50f528363e2fa01208c1f48b52e967029f9edacd4c
6
+ metadata.gz: 0dc95cf029ba6e32480b9a10c07d2f50106f0630b66f0385134181d84b1af1bfd4e0dff016f1d140a696aae180eaae83f173c6575e51a04a68bf7574f9177307
7
+ data.tar.gz: 5fdb3e8c5b8f08f3afb4769855595ccf2a8426ac429428f19bd32d82cec932d7882c892b60eb5f2e2ae8ae65822a91fc1ef86cc7701b6fa994fffe31912ddabe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - October 17, 2024
4
+
5
+ - Show running jobs
6
+ - Add charts
7
+ - Add auto-refresh
8
+ - Add number of current processes and recurring tasks to navbar
9
+ - Fix dark mode not switching for the first time
10
+
11
+ ## [0.1.1] - October 7, 2024
12
+
13
+ - Replace OpenStruct with a Hash to avoid including the `ostruct` gem
14
+
3
15
  ## [0.1.0] - October 7, 2024
4
16
 
5
17
  - Initial release
data/Procfile.dev CHANGED
@@ -1,2 +1,3 @@
1
1
  test_app: cd test_app && ./bin/rails server -p 3000
2
- tailwind: bun watch
2
+ jobs: cd test_app && ./bin/jobs
3
+ tailwind: bun watch
data/README.md CHANGED
@@ -1,14 +1,14 @@
1
- # Solid Queue Dashboard
1
+ # Solid Queue Dashboard <sup>BETA</sup>
2
2
 
3
3
  <p align="center">
4
4
  <a href="https://github.com/akodkod/solid-queue-dashboard#gh-light-mode-only">
5
- <img src="https://github.com/user-attachments/assets/90cb68cf-73d9-41bf-b9d5-93da7c36f204" alt="Solid Queue Dashboard Light Mode">
5
+ <img src="https://github.com/user-attachments/assets/55aa4a3c-da51-471b-8f58-0cf1f9a1f8da" alt="Solid Queue Dashboard Light Mode">
6
6
  </a>
7
7
  <a href="https://github.com/akodkod/solid-queue-dashboard#gh-dark-mode-only">
8
- <img src="https://github.com/user-attachments/assets/ea089277-67fc-4bc9-91d8-2e3e7349b0c9" alt="Solid Queue Dashboard Dark Mode">
8
+ <img src="https://github.com/user-attachments/assets/645558cb-c20f-4d4b-9697-55282710ea6c" alt="Solid Queue Dashboard Dark Mode">
9
9
  </a>
10
10
 
11
- _👋 I'm available for hire → [kodkod.me](https://kodkod.me)_
11
+ _👋 I'm Available for Hire → [kodkod.me](https://kodkod.me)_
12
12
  </p>
13
13
 
14
14
  ## Features
@@ -19,12 +19,12 @@
19
19
  - 📜 View execution history
20
20
  - 🔍 Filter options
21
21
  - 🔄 Retry jobs from the UI
22
- - đŸšĢ No dependencies
22
+ - đŸĨŦ Auto-refresh
23
+ - 📈 Add charts
23
24
  - 🐒 No monkey patching
25
+ - 💈 TailwindCSS
24
26
 
25
27
  ## Roadmap
26
- - 🏊‍♂ī¸ Auto-pooling
27
- - 📈 Add charts
28
28
  - 🚀 Manually trigger jobs
29
29
  - ⏚ī¸ Cancel long jobs (if possible)
30
30
  - 📊 More statistics and insights
@@ -43,7 +43,7 @@ bundle add solid_queue_dashboard
43
43
  Or add this line to your `Gemfile`:
44
44
 
45
45
  ```bash
46
- gem "solid_queue_dashboard", "~> 0.1.0"
46
+ gem "solid_queue_dashboard", "~> 0.2.0"
47
47
  ```
48
48
 
49
49
  Add this line to `routes.rb`:
@@ -85,10 +85,14 @@ To generate dummy data:
85
85
  ```
86
86
  cd test_app
87
87
  rails jobs:generate_dummy_data
88
- ./bin/jobs
89
88
  ```
90
89
 
91
90
  ## License
92
91
 
93
92
  This gem is open source under the [MIT License](http://opensource.org/licenses/MIT).
94
93
 
94
+ ---
95
+
96
+ _Made with love by Ukrainians 💙💛_
97
+ _[Help Ukraine](https://u24.gov.ua/)_
98
+
@@ -16,7 +16,7 @@ document.addEventListener('DOMContentLoaded', function() {
16
16
  });
17
17
 
18
18
  // Format dates
19
- document.querySelectorAll('[data-date]').forEach(function(element) {
19
+ document.querySelectorAll('[data-date]').forEach(function (element) {
20
20
  const dateString = element.textContent.trim();
21
21
  const date = new Date(dateString);
22
22
 
@@ -37,10 +37,24 @@ document.addEventListener('DOMContentLoaded', function() {
37
37
  });
38
38
 
39
39
  // Handle auto-submit elements
40
- document.querySelectorAll('[data-auto-submit]').forEach(function(element) {
41
- element.addEventListener('change', function() {
40
+ document.querySelectorAll('[data-auto-submit]').forEach(function (element) {
41
+ element.addEventListener('change', function () {
42
42
  const form = element.closest('form');
43
43
  if (form) form.submit();
44
44
  });
45
45
  });
46
+
47
+
48
+ // Auto-refresh functionality for dashboard page
49
+ const searchParams = new URLSearchParams(window.location.search);
50
+ const refreshInterval = parseInt(searchParams.get('auto_refresh_period'));
51
+
52
+ if (refreshInterval) {
53
+ setInterval(refreshHomePage, refreshInterval * 1000);
54
+ }
55
+
56
+ function refreshHomePage() {
57
+ // TODO: Implement a smart refresh strategy using Fetch or Turbo
58
+ window.location.reload();
59
+ }
46
60
  });
@@ -584,12 +584,12 @@ video {
584
584
 
585
585
  .dark {
586
586
  --background: 240 10% 5.4%;
587
- --foreground: 0 0% 98%;
587
+ --foreground: 0 0% 92%;
588
588
  --card: 240 10% 3.9%;
589
- --card-foreground: 0 0% 98%;
589
+ --card-foreground: 0 0% 92%;
590
590
  --popover: 240 10% 3.9%;
591
591
  --popover-foreground: 0 0% 98%;
592
- --primary: 0 0% 98%;
592
+ --primary: 0 0% 95%;
593
593
  --primary-foreground: 240 5.9% 10%;
594
594
  --secondary: 240 3.7% 15.9%;
595
595
  --secondary-foreground: 0 0% 98%;
@@ -796,6 +796,16 @@ body {
796
796
  background-color: hsl(var(--primary) / 0.8);
797
797
  }
798
798
 
799
+ .badge-secondary {
800
+ border-color: transparent;
801
+ background-color: hsl(var(--secondary));
802
+ color: hsl(var(--secondary-foreground));
803
+ }
804
+
805
+ .badge-secondary:hover {
806
+ background-color: hsl(var(--secondary) / 0.8);
807
+ }
808
+
799
809
  .badge-destructive {
800
810
  border-color: transparent;
801
811
  background-color: hsl(var(--destructive) / 0.15);
@@ -901,6 +911,18 @@ body {
901
911
  color: rgb(96 165 250 / var(--tw-text-opacity));
902
912
  }
903
913
 
914
+ .badge-indigo {
915
+ border-color: transparent;
916
+ background-color: rgb(99 102 241 / 0.15);
917
+ --tw-text-opacity: 1;
918
+ color: rgb(67 56 202 / var(--tw-text-opacity));
919
+ }
920
+
921
+ .badge-indigo:is(.dark *) {
922
+ --tw-text-opacity: 1;
923
+ color: rgb(129 140 248 / var(--tw-text-opacity));
924
+ }
925
+
904
926
  .badge-purple {
905
927
  border-color: transparent;
906
928
  background-color: rgb(168 85 247 / 0.15);
@@ -915,7 +937,7 @@ body {
915
937
 
916
938
  .badge-zinc {
917
939
  border-color: transparent;
918
- background-color: rgb(82 82 91 / 0.1);
940
+ background-color: rgb(82 82 91 / 0.075);
919
941
  --tw-text-opacity: 1;
920
942
  color: rgb(63 63 70 / var(--tw-text-opacity));
921
943
  }
@@ -969,6 +991,11 @@ body {
969
991
  background-color: rgb(59 130 246 / var(--tw-bg-opacity));
970
992
  }
971
993
 
994
+ .circle-indigo {
995
+ --tw-bg-opacity: 1;
996
+ background-color: rgb(99 102 241 / var(--tw-bg-opacity));
997
+ }
998
+
972
999
  .circle-purple {
973
1000
  --tw-bg-opacity: 1;
974
1001
  background-color: rgb(168 85 247 / var(--tw-bg-opacity));
@@ -976,7 +1003,7 @@ body {
976
1003
 
977
1004
  .circle-zinc {
978
1005
  --tw-bg-opacity: 1;
979
- background-color: rgb(82 82 91 / var(--tw-bg-opacity));
1006
+ background-color: rgb(161 161 170 / var(--tw-bg-opacity));
980
1007
  }
981
1008
 
982
1009
  /*
@@ -1504,6 +1531,22 @@ body {
1504
1531
  position: static;
1505
1532
  }
1506
1533
 
1534
+ .absolute {
1535
+ position: absolute;
1536
+ }
1537
+
1538
+ .relative {
1539
+ position: relative;
1540
+ }
1541
+
1542
+ .right-6 {
1543
+ right: 1.5rem;
1544
+ }
1545
+
1546
+ .top-6 {
1547
+ top: 1.5rem;
1548
+ }
1549
+
1507
1550
  .mx-auto {
1508
1551
  margin-left: auto;
1509
1552
  margin-right: auto;
@@ -1529,6 +1572,10 @@ body {
1529
1572
  margin-left: 0.125rem;
1530
1573
  }
1531
1574
 
1575
+ .ml-1 {
1576
+ margin-left: 0.25rem;
1577
+ }
1578
+
1532
1579
  .ml-1\.5 {
1533
1580
  margin-left: 0.375rem;
1534
1581
  }
@@ -1561,6 +1608,10 @@ body {
1561
1608
  margin-right: 0.375rem;
1562
1609
  }
1563
1610
 
1611
+ .mt-0\.5 {
1612
+ margin-top: 0.125rem;
1613
+ }
1614
+
1564
1615
  .mt-1 {
1565
1616
  margin-top: 0.25rem;
1566
1617
  }
@@ -1581,10 +1632,6 @@ body {
1581
1632
  margin-top: 2rem;
1582
1633
  }
1583
1634
 
1584
- .mt-0\.5 {
1585
- margin-top: 0.125rem;
1586
- }
1587
-
1588
1635
  .inline-block {
1589
1636
  display: inline-block;
1590
1637
  }
@@ -1641,6 +1688,10 @@ body {
1641
1688
  width: 6rem;
1642
1689
  }
1643
1690
 
1691
+ .w-40 {
1692
+ width: 10rem;
1693
+ }
1694
+
1644
1695
  .w-5 {
1645
1696
  width: 1.25rem;
1646
1697
  }
@@ -1667,8 +1718,8 @@ body {
1667
1718
  grid-template-columns: repeat(1, minmax(0, 1fr));
1668
1719
  }
1669
1720
 
1670
- .grid-cols-5 {
1671
- grid-template-columns: repeat(5, minmax(0, 1fr));
1721
+ .grid-cols-6 {
1722
+ grid-template-columns: repeat(6, minmax(0, 1fr));
1672
1723
  }
1673
1724
 
1674
1725
  .\!flex-row {
@@ -1683,6 +1734,10 @@ body {
1683
1734
  flex-wrap: wrap;
1684
1735
  }
1685
1736
 
1737
+ .items-start {
1738
+ align-items: flex-start;
1739
+ }
1740
+
1686
1741
  .items-center {
1687
1742
  align-items: center;
1688
1743
  }
@@ -1703,6 +1758,10 @@ body {
1703
1758
  gap: 0.25rem;
1704
1759
  }
1705
1760
 
1761
+ .gap-1\.5 {
1762
+ gap: 0.375rem;
1763
+ }
1764
+
1706
1765
  .gap-2 {
1707
1766
  gap: 0.5rem;
1708
1767
  }
@@ -1785,6 +1844,11 @@ body {
1785
1844
  border-top-color: rgb(239 68 68 / var(--tw-border-opacity));
1786
1845
  }
1787
1846
 
1847
+ .border-t-sky-500 {
1848
+ --tw-border-opacity: 1;
1849
+ border-top-color: rgb(14 165 233 / var(--tw-border-opacity));
1850
+ }
1851
+
1788
1852
  .border-t-zinc-500 {
1789
1853
  --tw-border-opacity: 1;
1790
1854
  border-top-color: rgb(113 113 122 / var(--tw-border-opacity));
@@ -1798,6 +1862,14 @@ body {
1798
1862
  padding: 0px !important;
1799
1863
  }
1800
1864
 
1865
+ .p-0 {
1866
+ padding: 0px;
1867
+ }
1868
+
1869
+ .p-6 {
1870
+ padding: 1.5rem;
1871
+ }
1872
+
1801
1873
  .px-2 {
1802
1874
  padding-left: 0.5rem;
1803
1875
  padding-right: 0.5rem;
@@ -1822,6 +1894,10 @@ body {
1822
1894
  padding-bottom: 15vh;
1823
1895
  }
1824
1896
 
1897
+ .pl-1 {
1898
+ padding-left: 0.25rem;
1899
+ }
1900
+
1825
1901
  .pl-6 {
1826
1902
  padding-left: 1.5rem;
1827
1903
  }
@@ -1867,19 +1943,6 @@ body {
1867
1943
  line-height: 1rem;
1868
1944
  }
1869
1945
 
1870
- .text-base {
1871
- font-size: 1rem;
1872
- line-height: 1.5rem;
1873
- }
1874
-
1875
- .text-\[15px\] {
1876
- font-size: 15px;
1877
- }
1878
-
1879
- .text-\[14px\] {
1880
- font-size: 14px;
1881
- }
1882
-
1883
1946
  .font-bold {
1884
1947
  font-weight: 700;
1885
1948
  }
@@ -1964,6 +2027,10 @@ body {
1964
2027
  }
1965
2028
  }
1966
2029
 
2030
+ .running {
2031
+ animation-play-state: running;
2032
+ }
2033
+
1967
2034
  .marker\:text-sm *::marker {
1968
2035
  font-size: 0.875rem;
1969
2036
  line-height: 1.25rem;
@@ -2009,6 +2076,11 @@ body {
2009
2076
  border-top-color: rgb(147 51 234 / var(--tw-border-opacity));
2010
2077
  }
2011
2078
 
2079
+ .dark\:border-t-sky-600:is(.dark *) {
2080
+ --tw-border-opacity: 1;
2081
+ border-top-color: rgb(2 132 199 / var(--tw-border-opacity));
2082
+ }
2083
+
2012
2084
  .dark\:border-t-zinc-600:is(.dark *) {
2013
2085
  --tw-border-opacity: 1;
2014
2086
  border-top-color: rgb(82 82 91 / var(--tw-border-opacity));
@@ -33,12 +33,12 @@
33
33
 
34
34
  .dark {
35
35
  --background: 240 10% 5.4%;
36
- --foreground: 0 0% 98%;
36
+ --foreground: 0 0% 92%;
37
37
  --card: 240 10% 3.9%;
38
- --card-foreground: 0 0% 98%;
38
+ --card-foreground: 0 0% 92%;
39
39
  --popover: 240 10% 3.9%;
40
40
  --popover-foreground: 0 0% 98%;
41
- --primary: 0 0% 98%;
41
+ --primary: 0 0% 95%;
42
42
  --primary-foreground: 240 5.9% 10%;
43
43
  --secondary: 240 3.7% 15.9%;
44
44
  --secondary-foreground: 0 0% 98%;
@@ -188,7 +188,7 @@
188
188
  }
189
189
 
190
190
  .badge-zinc {
191
- @apply border-transparent bg-zinc-600/10 text-zinc-700 dark:bg-white/5 dark:text-zinc-400;
191
+ @apply border-transparent bg-zinc-600/7.5 text-zinc-700 dark:bg-white/5 dark:text-zinc-400;
192
192
  }
193
193
 
194
194
  /*
@@ -284,7 +284,7 @@
284
284
  }
285
285
 
286
286
  .circle-zinc {
287
- @apply bg-zinc-600;
287
+ @apply bg-zinc-400;
288
288
  }
289
289
 
290
290
  /*
@@ -1,7 +1,7 @@
1
1
  module SolidQueueDashboard
2
2
  class AppearanceController < ApplicationController
3
3
  def toggle
4
- cookies[:dark_mode] = cookies[:dark_mode] == "false" ? "true" : "false"
4
+ cookies[:dark_mode] = cookies[:dark_mode] == "true" ? "false" : "true"
5
5
  redirect_to request.referer
6
6
  end
7
7
  end
@@ -2,7 +2,67 @@ module SolidQueueDashboard
2
2
  class DashboardController < ApplicationController
3
3
  def index
4
4
  @jobs = SolidQueueDashboard.decorate(SolidQueue::Job.all)
5
- @job_class_names = SolidQueueDashboard.job_class_names
5
+ load_charts
6
+ end
7
+
8
+ private
9
+
10
+ def load_charts
11
+ case params[:chart_period] || "30m"
12
+ when "15m"
13
+ n = 1
14
+ last = 15
15
+ when "30m"
16
+ n = 1
17
+ last = 30
18
+ when "1h"
19
+ n = 2
20
+ last = 30
21
+ when "3h"
22
+ n = 6
23
+ last = 30
24
+ when "6h"
25
+ n = 12
26
+ last = 30
27
+ when "12h"
28
+ n = 20
29
+ last = 36
30
+ when "1d"
31
+ n = 30
32
+ last = 48
33
+ when "3d"
34
+ n = 90 # 1.5 hours
35
+ last = 48
36
+ when "1w"
37
+ n = 180 # 3 hours
38
+ last = 56
39
+ else
40
+ n = 1
41
+ last = 30
42
+ end
43
+
44
+ @charts = [
45
+ {
46
+ name: "Enqueued",
47
+ data: SolidQueue::Job.group_by_minute(:created_at, last: last, n: n).count,
48
+ color: "#A1A1AB"
49
+ },
50
+ {
51
+ name: "Finished",
52
+ data: @jobs.finished.group_by_minute(:finished_at, last: last, n: n).count,
53
+ color: "#23C55E"
54
+ },
55
+ {
56
+ name: "Retried",
57
+ data: @jobs.retried.group_by_minute(:finished_at, last: last, n: n).count,
58
+ color: "#FBBF26"
59
+ },
60
+ {
61
+ name: "Failed",
62
+ data: SolidQueue::FailedExecution.group_by_minute(:created_at, last: last, n: n).count,
63
+ color: "#F04444"
64
+ }
65
+ ]
6
66
  end
7
67
  end
8
68
  end
@@ -32,7 +32,7 @@ module SolidQueueDashboard
32
32
  jobs = jobs.where(queue_name: params[:queue_name]) if params[:queue_name].present?
33
33
 
34
34
  @pagination = paginate(jobs, page: params[:page].to_i, per_page: params[:per_page].to_i)
35
- @jobs = SolidQueueDashboard.decorate(@pagination.records)
35
+ @jobs = SolidQueueDashboard.decorate(@pagination[:records])
36
36
  end
37
37
 
38
38
  def set_job
@@ -0,0 +1,8 @@
1
+ module SolidQueueDashboard
2
+ class StatsController < ApplicationController
3
+ def index
4
+ @jobs = SolidQueueDashboard.decorate(SolidQueue::Job.all)
5
+ @job_class_names = SolidQueueDashboard.job_class_names
6
+ end
7
+ end
8
+ end
@@ -227,5 +227,28 @@ module SolidQueueDashboard
227
227
  concat tag.polygon(points: "6 3 20 12 6 21 6 3")
228
228
  end
229
229
  end
230
+
231
+ def icon_chart_scatter(options = {})
232
+ svg_options = {
233
+ xmlns: "http://www.w3.org/2000/svg",
234
+ width: "24",
235
+ height: "24",
236
+ viewBox: "0 0 24 24",
237
+ fill: "none",
238
+ stroke: "currentColor",
239
+ "stroke-width": "2",
240
+ "stroke-linecap": "round",
241
+ "stroke-linejoin": "round"
242
+ }.merge(options)
243
+
244
+ tag.svg(**svg_options) do
245
+ concat tag.circle(cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor")
246
+ concat tag.circle(cx: "18.5", cy: "5.5", r: ".5", fill: "currentColor")
247
+ concat tag.circle(cx: "11.5", cy: "11.5", r: ".5", fill: "currentColor")
248
+ concat tag.circle(cx: "7.5", cy: "16.5", r: ".5", fill: "currentColor")
249
+ concat tag.circle(cx: "17.5", cy: "14.5", r: ".5", fill: "currentColor")
250
+ concat tag.path(d: "M3 3v16a2 2 0 0 0 2 2h16")
251
+ end
252
+ end
230
253
  end
231
254
  end
@@ -11,7 +11,10 @@ module SolidQueueDashboard
11
11
  "amber": "circle-amber",
12
12
  "red": "circle-red",
13
13
  "blue": "circle-blue",
14
- "zinc": "circle-zinc"
14
+ "sky": "circle-sky",
15
+ "zinc": "circle-zinc",
16
+ "indigo": "circle-indigo",
17
+ "purple": "circle-purple"
15
18
  }[Job::STATUS_COLORS[status]&.to_sym || :zinc]
16
19
  end
17
20
 
@@ -26,7 +29,10 @@ module SolidQueueDashboard
26
29
  "amber": "badge-amber",
27
30
  "red": "badge-red",
28
31
  "blue": "badge-blue",
29
- "zinc": "badge-zinc"
32
+ "sky": "badge-sky",
33
+ "zinc": "badge-zinc",
34
+ "indigo": "badge-indigo",
35
+ "purple": "badge-purple"
30
36
  }[Job::STATUS_COLORS[status]&.to_sym || :zinc]
31
37
  end
32
38
 
@@ -10,13 +10,13 @@ module SolidQueueDashboard
10
10
  total_count = scope.count
11
11
  total_pages = (total_count.to_f / per_page).ceil
12
12
 
13
- OpenStruct.new(
13
+ {
14
14
  records: records,
15
15
  current_page: page,
16
16
  per_page: per_page,
17
17
  total_pages: total_pages,
18
18
  total_count: total_count
19
- )
19
+ }
20
20
  end
21
21
 
22
22
  def page_range(current_page, total_pages, window: 2)
@@ -10,7 +10,8 @@ module SolidQueueDashboard
10
10
  "blue": "circle-blue",
11
11
  "green": "circle-green",
12
12
  "yellow": "circle-yellow",
13
- "purple": "circle-purple"
13
+ "purple": "circle-purple",
14
+ "sky": "circle-sky",
14
15
  }[Process::KIND_COLORS[kind]&.to_sym || :zinc]
15
16
  end
16
17
 
@@ -24,7 +25,8 @@ module SolidQueueDashboard
24
25
  "blue": "badge-blue",
25
26
  "green": "badge-green",
26
27
  "yellow": "badge-yellow",
27
- "purple": "badge-purple"
28
+ "purple": "badge-purple",
29
+ "sky": "badge-sky",
28
30
  }[Process::KIND_COLORS[kind]&.to_sym || :zinc]
29
31
  end
30
32
 
@@ -8,14 +8,16 @@
8
8
 
9
9
  <%= stylesheet_link_tag "solid_queue_dashboard/application", media: "all" %>
10
10
  <%= javascript_include_tag "solid_queue_dashboard/application" %>
11
+ <%= javascript_include_tag "chartkick" %>
12
+ <%= javascript_include_tag "Chart.bundle" %>
11
13
  <%= javascript_include_tag "solid_queue_dashboard/alpine", defer: true %>
12
14
  </head>
13
15
  <body class="pb-[15vh] sm:pb-[25vh]">
14
16
  <div class="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8">
15
- <%= render 'navbar' %>
16
- <%= render 'flash_messages' %>
17
+ <%= render "navbar" %>
18
+ <%= render "flash_messages" %>
17
19
  <%= yield %>
18
- <%= render 'footer' %>
20
+ <%= render "footer" %>
19
21
  </div>
20
22
  </body>
21
23
  </html>
@@ -1,5 +1,5 @@
1
1
  <nav class="navbar mb-6">
2
- <%= link_to root_path, class: "inline-flex items-center gap-0.5 text-xl font-bold tracking-tight translate-y-px" do %>
2
+ <%= link_to root_path, class: "inline-flex items-center gap-0.5 text-xl font-bold tracking-tight translate-y-px pl-1" do %>
3
3
  <span class="circle circle-blue"></span>
4
4
  <span class="circle circle-green"></span>
5
5
  <span class="circle circle-yellow"></span>
@@ -21,18 +21,41 @@
21
21
  <%= link_to processes_path, class: "navbar-item #{current_page?(processes_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
22
22
  <%= icon_server class: "size-4 mr-1.5" %>
23
23
  Processes
24
+
25
+ <span class="badge <%= current_page?(processes_path) ? 'badge-secondary' : 'badge-primary' %> ml-1 text-xs">
26
+ <%= SolidQueue::Process.count %>
27
+ </span>
24
28
  <% end %>
25
29
 
26
30
  <%= link_to recurring_tasks_path, class: "navbar-item #{current_page?(recurring_tasks_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
27
31
  <%= icon_clock class: "size-4 mr-1.5" %>
28
32
  Recurring Tasks
33
+
34
+ <span class="badge <%= current_page?(recurring_tasks_path) ? 'badge-secondary' : 'badge-primary' %> ml-1 text-xs">
35
+ <%= SolidQueue::RecurringTask.count %>
36
+ </span>
37
+ <% end %>
38
+
39
+ <%= link_to stats_path, class: "navbar-item #{current_page?(stats_path) ? 'navbar-item-current' : 'navbar-item-default'}" do %>
40
+ <%= icon_chart_scatter class: "size-4 mr-1.5" %>
41
+ Stats
29
42
  <% end %>
30
43
  </div>
31
44
 
32
- <div class="ml-auto">
33
- <div>
34
-
35
- </div>
45
+ <div class="ml-auto flex gap-4">
46
+ <% if current_page?(controller: 'dashboard', action: 'index') %>
47
+ <%= form_with url: root_path, method: :get, class: "flex gap-1.5 items-center" do |form| %>
48
+ <%= form.hidden_field :chart_period %>
49
+
50
+ <%= icon_refresh_cw class: "size-4 text-muted-foreground" %>
51
+ <%= form.select :auto_refresh_period,
52
+ [["Auto-Refresh Off", "off"], ["15 seconds", "15"], ["30 seconds", "30"], ["1 minute", "60"], ["3 minutes", "180"], ["5 minutes", "300"], ["10 minutes", "600"], ["15 minutes", "900"], ["30 minutes", "1800"], ["1 hour", "3600"]],
53
+ { selected: params[:auto_refresh_period].presence || "off" },
54
+ class: "select w-40",
55
+ data: { auto_submit: true }
56
+ %>
57
+ <% end %>
58
+ <% end %>
36
59
 
37
60
  <%= form_with url: toggle_appearance_path, method: :post do |f| %>
38
61
  <button
@@ -1,4 +1,18 @@
1
- <div class="grid grid-cols-5 gap-4">
1
+ <div class="card card-content p-6 relative">
2
+ <%= form_with url: root_path, method: :get, class: "absolute top-6 right-6" do |form| %>
3
+ <%= form.hidden_field :auto_refresh_period %>
4
+ <%= form.select :chart_period,
5
+ [["15 minutes", "15m"], ["30 minutes", "30m"], ["1 hour", "1h"], ["3 hours", "3h"], ["6 hours", "6h"], ["12 hours", "12h"], ["1 day", "1d"], ["3 days", "3d"], ["1 week", "1w"]],
6
+ { selected: params[:chart_period].presence || "30m" },
7
+ class: "select",
8
+ data: { auto_submit: true }
9
+ %>
10
+ <% end %>
11
+
12
+ <%= line_chart @charts, points: false, thousands: "," %>
13
+ </div>
14
+
15
+ <div class="grid grid-cols-6 gap-4 mt-4">
2
16
  <% SolidQueueDashboard::Job::STATUSES.each do |status| %>
3
17
  <div class="card" data-href="<%= jobs_path(status:) %>">
4
18
  <div class="card-content pt-5">
@@ -7,35 +21,9 @@
7
21
  <span class="ml-0.5 -translate-y-px circle <%= job_status_circle_class(status) %>"></span>
8
22
  </h4>
9
23
  <p class="text-4xl font-bold mt-1 text-black dark:text-white">
10
- <%= @jobs.with_status(status).count %>
24
+ <%= number_with_delimiter(@jobs.with_status(status).count) %>
11
25
  </p>
12
26
  </div>
13
27
  </div>
14
28
  <% end %>
15
29
  </div>
16
-
17
- <div class="card mt-4">
18
- <div class="card-header border-b">
19
- <h3 class="card-title">Failure Rate</h3>
20
- </div>
21
- <div class="card-content !p-0">
22
- <div class="table-wrapper">
23
- <table class="table">
24
- <thead class="table-header">
25
- <tr class="table-row">
26
- <th class="table-head">Job</th>
27
- <th class="table-head">Failure Rate</th>
28
- </tr>
29
- </thead>
30
- <tbody class="table-body">
31
- <% @job_class_names.each do |class_name| %>
32
- <tr class="table-row" data-href="<%= jobs_path(class_name:, status: :failed) %>">
33
- <td class="table-cell font-medium"><%= class_name.titleize %></td>
34
- <td class="table-cell font-medium"><%= format_failure_rate(SolidQueueDashboard.decorate(SolidQueue::Job.where(class_name:)).failure_rate) %></td>
35
- </tr>
36
- <% end %>
37
- </tbody>
38
- </table>
39
- </div>
40
- </div>
41
- </div>
@@ -13,7 +13,7 @@
13
13
  </tr>
14
14
  </thead>
15
15
  <tbody class="table-body">
16
- <%= render partial: 'solid_queue_dashboard/jobs/table_row', collection: jobs, as: :job, locals: { highlight_ids: highlight_ids } %>
16
+ <%= render partial: "solid_queue_dashboard/jobs/table_row", collection: jobs, as: :job, locals: { highlight_ids: highlight_ids } %>
17
17
  </tbody>
18
18
  </table>
19
19
  </div>
@@ -14,6 +14,12 @@
14
14
 
15
15
  <td class="table-cell">
16
16
  <%= job_status_badge(job.status) %>
17
+ <% if job.running? %>
18
+ <br />
19
+ <span class="text-xs text-muted-foreground">
20
+ <%= time_ago_in_words(job.claimed_execution.created_at, include_seconds: true) %>
21
+ </span>
22
+ <% end %>
17
23
  </td>
18
24
 
19
25
  <td class="table-cell">
@@ -29,9 +35,9 @@
29
35
  <p class="font-medium"><%= job.class_name %></p>
30
36
 
31
37
  <% if job.arguments["arguments"].present? %>
32
- <p class="mt-0.5">
38
+ <p class="inline-flex flex-wrap gap-0.5 items-start mt-0.5">
33
39
  <% job.arguments["arguments"].each do |argument| %>
34
- <span class="badge badge-zinc"><%= truncate(JSON.pretty_generate(argument), length: 80) %></span>
40
+ <span class="badge badge-zinc text-xs"><%= truncate(JSON.pretty_generate(argument), length: 60) %></span>
35
41
  <% end %>
36
42
  </p>
37
43
  <% end %>
@@ -51,7 +57,17 @@
51
57
  </td>
52
58
 
53
59
  <td class="table-cell">
54
- <% if job.success? || job.retried? %>
60
+ <% if job.running? %>
61
+ <p class="text-sm text-muted-foreground">
62
+ Running for <strong class="font-medium text-foreground"><%= time_ago_in_words(job.claimed_execution.created_at, include_seconds: true) %></strong>
63
+ <% if job.claimed_execution.process %>
64
+ by <strong class="font-medium text-foreground">
65
+ <%= process_kind_badge(job.claimed_execution.process.kind) %>
66
+ <%= link_to "##{job.claimed_execution.process.id}", process_path(job.claimed_execution.process), class: "link" %>
67
+ </strong>
68
+ <% end %>
69
+ </p>
70
+ <% elsif job.success? || job.retried? %>
55
71
  <p class="text-sm text-muted-foreground">
56
72
  <%= job.retried? ? "Failed" : "Finished" %> at <strong class="font-medium text-foreground" data-date="<%= job.finished_at.to_fs(:database) %>"><%= job.finished_at %></strong><br />
57
73
  <span class="text-xs"><%= time_ago_in_words(job.scheduled_at, include_seconds: true) %> ago</span>
@@ -25,12 +25,12 @@
25
25
  <% end %>
26
26
  </div>
27
27
 
28
- <% if @pagination.total_pages > 1 %>
28
+ <% if @pagination[:total_pages] > 1 %>
29
29
  <div class="card-footer pt-6 border-t flex justify-between">
30
30
  <%= render partial: 'solid_queue_dashboard/application/pagination', locals: {
31
- current_page: @pagination.current_page,
32
- total_pages: @pagination.total_pages,
33
- per_page: @pagination.per_page
31
+ current_page: @pagination[:current_page],
32
+ total_pages: @pagination[:total_pages],
33
+ per_page: @pagination[:per_page]
34
34
  } %>
35
35
 
36
36
  <div class="flex items-center gap-2">
@@ -42,7 +42,7 @@
42
42
  <%= f.hidden_field :page, value: 1 %>
43
43
  <%= f.select :per_page,
44
44
  [10, 25, 50, 100],
45
- { selected: @pagination.per_page },
45
+ { selected: @pagination[:per_page] },
46
46
  class: "select w-24",
47
47
  data: { auto_submit: true }
48
48
  %>
@@ -126,9 +126,11 @@
126
126
  <% if @job.class_name === SolidQueueDashboard::Job::COMMAND_CLASS_NAME %>
127
127
  <pre x-show="!showAll"><%= @job.arguments["arguments"][0] %></pre>
128
128
  <% else %>
129
- <% @job.arguments["arguments"].each do |argument| %>
130
- <span class="badge badge-zinc text-sm"><%= JSON.pretty_generate(argument) %></span>
131
- <% end %>
129
+ <div x-show="!showAll">
130
+ <% @job.arguments["arguments"].each do |argument| %>
131
+ <span class="badge badge-zinc text-sm"><%= JSON.pretty_generate(argument) %></span>
132
+ <% end %>
133
+ </div>
132
134
  <% end %>
133
135
 
134
136
  <pre x-show="showAll"><%= JSON.pretty_generate(@job.arguments) %></pre>
@@ -5,6 +5,12 @@
5
5
 
6
6
  <td class="table-cell">
7
7
  <%= process_kind_badge(process.kind) %>
8
+
9
+ <% if process.claimed_executions.any? %>
10
+ <span class="ml-0.5 text-xs text-muted-foreground">
11
+ Running <%= pluralize(process.claimed_executions.count, "job") %>
12
+ </span>
13
+ <% end %>
8
14
  </td>
9
15
 
10
16
  <td class="table-cell"><%= process.hostname %></td>
@@ -66,9 +66,22 @@
66
66
  </div>
67
67
  </div>
68
68
 
69
+ <% if @process.claimed_executions.any? %>
70
+ <div class="card mt-8 overflow-hidden">
71
+ <div class="card-header border-b border-t-4 border-t-sky-500 dark:border-t-sky-600">
72
+ <h2 class="card-title">Running Jobs</h2>
73
+ <p class="card-description">Jobs that this process is currently running</p>
74
+ </div>
75
+
76
+ <div class="card-content p-0">
77
+ <%= render partial: 'solid_queue_dashboard/jobs/table', locals: { jobs: @process.claimed_executions.map { |execution| SolidQueueDashboard::Decorators::JobDecorator.new(execution.job) } } %>
78
+ </div>
79
+ </div>
80
+ <% end %>
81
+
69
82
  <% if @process.metadata.present? %>
70
83
  <div class="card mt-8 overflow-hidden">
71
- <div class="card-header border-b border-t-4 border-t-blue-500 dark:border-t-blue-600">
84
+ <div class="card-header border-b border-t-4 border-t-amber-500 dark:border-t-amber-600">
72
85
  <h2 class="card-title">Metadata</h2>
73
86
  <p class="card-description">Additional information about this process</p>
74
87
  </div>
@@ -122,7 +122,12 @@
122
122
  <div class="card-content pt-5">
123
123
  <ul class="list-decimal marker:text-sm marker:text-muted-foreground ml-4 space-y-1">
124
124
  <% @recurring_task.next_runs.each do |run_time| %>
125
- <li data-date><%= run_time.strftime("%Y-%m-%d %H:%M:%S %Z") %></li>
125
+ <li>
126
+ <span data-date><%= run_time.strftime("%Y-%m-%d %H:%M:%S %Z") %></span>
127
+ <span class="text-muted-foreground">
128
+ in <%= distance_of_time_in_words(Time.current, run_time) %>
129
+ </span>
130
+ </li>
126
131
  <% end %>
127
132
  </ul>
128
133
  </div>
@@ -0,0 +1,25 @@
1
+ <div class="card mt-4">
2
+ <div class="card-header border-b">
3
+ <h3 class="card-title">Failure Rate</h3>
4
+ </div>
5
+ <div class="card-content !p-0">
6
+ <div class="table-wrapper">
7
+ <table class="table">
8
+ <thead class="table-header">
9
+ <tr class="table-row">
10
+ <th class="table-head">Job</th>
11
+ <th class="table-head">Failure Rate</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody class="table-body">
15
+ <% @job_class_names.each do |class_name| %>
16
+ <tr class="table-row" data-href="<%= jobs_path(class_name:, status: :failed) %>">
17
+ <td class="table-cell font-medium"><%= class_name.titleize %></td>
18
+ <td class="table-cell font-medium"><%= format_failure_rate(SolidQueueDashboard.decorate(SolidQueue::Job.where(class_name:)).failure_rate) %></td>
19
+ </tr>
20
+ <% end %>
21
+ </tbody>
22
+ </table>
23
+ </div>
24
+ </div>
25
+ </div>
data/config/routes.rb CHANGED
@@ -13,6 +13,8 @@ SolidQueueDashboard::Engine.routes.draw do
13
13
  end
14
14
  end
15
15
 
16
+ get "stats", to: "stats#index", as: :stats
16
17
  post "appearance/toggle", to: "appearance#toggle", as: :toggle_appearance
18
+
17
19
  root "dashboard#index"
18
20
  end
@@ -8,7 +8,9 @@ module SolidQueueDashboard
8
8
  def status
9
9
  return @status if defined?(@status)
10
10
 
11
- @status = if retried?
11
+ @status = if running?
12
+ Job::RUNNING
13
+ elsif retried?
12
14
  Job::RETRIED
13
15
  elsif failed?
14
16
  Job::FAILED
@@ -21,6 +23,11 @@ module SolidQueueDashboard
21
23
  end
22
24
  end
23
25
 
26
+ def running?
27
+ return @running if defined?(@running)
28
+ @running = claimed_execution.present?
29
+ end
30
+
24
31
  def success?
25
32
  return @success if defined?(@success)
26
33
  @success = finished_at.present? && !failed? && !retried?
@@ -44,7 +51,7 @@ module SolidQueueDashboard
44
51
 
45
52
  def pending?
46
53
  return @pending if defined?(@pending)
47
- @pending = !finished_at.present?
54
+ @pending = !finished_at.present? && !running?
48
55
  end
49
56
 
50
57
  def execution_history
@@ -3,6 +3,8 @@ module SolidQueueDashboard
3
3
  class JobsDecorator < SimpleDelegator
4
4
  def with_status(status)
5
5
  case status.to_sym
6
+ when Job::RUNNING
7
+ running
6
8
  when Job::SUCCESS
7
9
  success
8
10
  when Job::FAILED
@@ -18,6 +20,10 @@ module SolidQueueDashboard
18
20
  end
19
21
  end
20
22
 
23
+ def running
24
+ where.associated(:claimed_execution)
25
+ end
26
+
21
27
  def success
22
28
  where.not(finished_at: nil)
23
29
  .where.not(id: failed)
@@ -31,6 +37,7 @@ module SolidQueueDashboard
31
37
  def pending
32
38
  where(finished_at: nil, scheduled_at: ..Time.current)
33
39
  .where.not(id: failed)
40
+ .where.not(id: running)
34
41
  end
35
42
 
36
43
  def retried
@@ -1,20 +1,22 @@
1
1
  module SolidQueueDashboard
2
2
  module Job
3
3
  # Constants
4
+ RUNNING = :running
4
5
  SUCCESS = :success
5
6
  RETRIED = :retried
6
7
  FAILED = :failed
7
8
  PENDING = :pending
8
9
  SCHEDULED = :scheduled
9
10
 
10
- STATUSES = [ SUCCESS, RETRIED, FAILED, SCHEDULED, PENDING ]
11
+ STATUSES = [ RUNNING, SUCCESS, RETRIED, FAILED, SCHEDULED, PENDING ]
11
12
 
12
13
  STATUS_COLORS = {
13
14
  SUCCESS => "green",
14
15
  RETRIED => "amber",
15
16
  FAILED => "red",
16
- SCHEDULED => "blue",
17
- PENDING => "zinc"
17
+ SCHEDULED => "purple",
18
+ PENDING => "zinc",
19
+ RUNNING => "sky"
18
20
  }
19
21
 
20
22
  COMMAND_CLASS_NAME = "SolidQueue::RecurringJob"
@@ -9,13 +9,13 @@ module SolidQueueDashboard
9
9
  KINDS = [ SUPERVISOR, DISPATCHER, WORKER, SCHEDULER ]
10
10
 
11
11
  KIND_COLORS = {
12
- SUPERVISOR => "blue",
12
+ SUPERVISOR => "yellow",
13
13
  DISPATCHER => "green",
14
- WORKER => "yellow",
14
+ WORKER => "sky",
15
15
  SCHEDULER => "purple"
16
16
  }
17
17
 
18
- HEARTBEAT_DEAD_THRESHOLD = 1.minute
18
+ HEARTBEAT_DEAD_THRESHOLD = 3.minutes
19
19
 
20
20
  def self.kind_color(kind)
21
21
  KIND_COLORS[kind] || "zinc"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueDashboard
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
+ require "groupdate"
5
+ require "chartkick"
4
6
  require_relative "solid_queue_dashboard/version"
5
7
  require_relative "solid_queue_dashboard/configuration"
6
8
  require_relative "solid_queue_dashboard/engine"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kodkod
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-07 00:00:00.000000000 Z
11
+ date: 2024-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: solid_queue
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: groupdate
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: chartkick
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
27
55
  description: Dashboard for Solid Queue
28
56
  email:
29
57
  - andrew@kodkod.me
@@ -48,6 +76,7 @@ files:
48
76
  - app/controllers/solid_queue_dashboard/jobs_controller.rb
49
77
  - app/controllers/solid_queue_dashboard/processes_controller.rb
50
78
  - app/controllers/solid_queue_dashboard/recurring_tasks_controller.rb
79
+ - app/controllers/solid_queue_dashboard/stats_controller.rb
51
80
  - app/helpers/solid_queue_dashboard/appearance_helper.rb
52
81
  - app/helpers/solid_queue_dashboard/application_helper.rb
53
82
  - app/helpers/solid_queue_dashboard/icons_helper.rb
@@ -76,6 +105,7 @@ files:
76
105
  - app/views/solid_queue_dashboard/recurring_tasks/_table_row.html.erb
77
106
  - app/views/solid_queue_dashboard/recurring_tasks/index.html.erb
78
107
  - app/views/solid_queue_dashboard/recurring_tasks/show.html.erb
108
+ - app/views/solid_queue_dashboard/stats/index.html.erb
79
109
  - bun.lockb
80
110
  - config/routes.rb
81
111
  - lib/solid_queue_dashboard.rb