dbviewer 0.5.5 → 0.5.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f0218efe5ea6bc79f61ca87472b6b413916b82e3aedf28e6030c37f302b2ca3
4
- data.tar.gz: 65c58c041d85f80f0ad609e54225ec9c5bf3707bc36954ea07c36b95eb69a23c
3
+ metadata.gz: 3385fea543f18e7af2bd9a924b5095f76235612d569fc1e1a828222b8b1a3b53
4
+ data.tar.gz: 762f1ee1b3774c9186cbdac735beb0717c6a3feb64ffecd7833ec8732ce91898
5
5
  SHA512:
6
- metadata.gz: 15b2f9f0cf3543c59bc0230ef23070b93d64c7038387e8d384aa2613ef0f28badec97957c2b3d48ab991da86f22f48e9fd71a41844d9247d4cf3d8c015f46331
7
- data.tar.gz: 05a6f0cfdd61f901a6dd17edbcd2a93c5244f461a4bfe61b350c2ed864af7e83ed1afa6489a8d2d0e81a66b307d0aa65ea8b81c080a4dc3cca896cec4ee20598
6
+ metadata.gz: a1552e63407dd92eb3bfa821a4712f6cb57b5c68462c6028a9e3debe14f2e72ce073c631ec1616aa50a5158939ae667899ef4999c2ac5feff8a6f22edd783b08
7
+ data.tar.gz: 2852a40ddec1a2a86d2b9764bf527fd21dd140aa57a7ef36da6d7dcf312362c6b24b753c6badd04aee89328557136c954adbad95a442540a233f53affac5489c
@@ -40,13 +40,7 @@
40
40
  <% @tables.each do |table| %>
41
41
  <tr>
42
42
  <td class="fw-medium">
43
- <%
44
- # Include creation filter params in table links
45
- filter_params = {}
46
- filter_params[:creation_filter_start] = session[:creation_filter_start] if session[:creation_filter_start].present?
47
- filter_params[:creation_filter_end] = session[:creation_filter_end] if session[:creation_filter_end].present?
48
- %>
49
- <%= link_to table[:name], table_path(table[:name], filter_params), class: "text-decoration-none" %>
43
+ <%= link_to table[:name], table_path(table[:name]), class: "text-decoration-none" %>
50
44
  </td>
51
45
  <td class="text-end">
52
46
  <span class="badge bg-secondary-subtle"><%= table[:record_count] %></span>
@@ -1525,3 +1525,1006 @@
1525
1525
  return section;
1526
1526
  }
1527
1527
  </script>
1528
+
1529
+ <!-- Floating Creation Filter - Only visible on desktop and on table details page -->
1530
+ <% if has_timestamp_column?(@table_name) %>
1531
+ <div class="floating-creation-filter d-none d-lg-block">
1532
+ <button class="btn btn-primary btn-lg shadow-lg floating-filter-btn"
1533
+ type="button"
1534
+ data-bs-toggle="offcanvas"
1535
+ data-bs-target="#creationFilterOffcanvas"
1536
+ aria-controls="creationFilterOffcanvas"
1537
+ title="Creation Date Filter">
1538
+ <i class="bi bi-calendar-range"></i>
1539
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1540
+ <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-success">
1541
+ <i class="bi bi-check"></i>
1542
+ </span>
1543
+ <% end %>
1544
+ </button>
1545
+ </div>
1546
+
1547
+ <!-- Creation Filter Offcanvas -->
1548
+ <div class="offcanvas offcanvas-end" tabindex="-1" id="creationFilterOffcanvas" aria-labelledby="creationFilterOffcanvasLabel">
1549
+ <div class="offcanvas-header">
1550
+ <h5 class="offcanvas-title" id="creationFilterOffcanvasLabel">
1551
+ <i class="bi bi-calendar-range me-2"></i>Creation Date Filter
1552
+ </h5>
1553
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
1554
+ </div>
1555
+ <div class="offcanvas-body">
1556
+ <form id="floatingCreationFilterForm" action="<%= request.path %>" method="get" class="mb-0">
1557
+ <!-- Preserve existing query parameters -->
1558
+ <input type="hidden" name="page" value="<%= @current_page %>">
1559
+ <input type="hidden" name="per_page" value="<%= @per_page %>">
1560
+ <input type="hidden" name="order_by" value="<%= @order_by %>">
1561
+ <input type="hidden" name="order_direction" value="<%= @order_direction %>">
1562
+
1563
+ <!-- Preserve column filters -->
1564
+ <% @column_filters.each do |key, value| %>
1565
+ <% unless key.to_s.start_with?('created_at') %>
1566
+ <input type="hidden" name="column_filters[<%= key %>]" value="<%= value %>">
1567
+ <% end %>
1568
+ <% end %>
1569
+
1570
+ <div class="mb-3">
1571
+ <p class="text-muted small">
1572
+ <i class="bi bi-info-circle me-1"></i>
1573
+ Filter records by their creation date and time. This applies to the <code>created_at</code> column.
1574
+ </p>
1575
+ </div>
1576
+
1577
+ <!-- Date range picker -->
1578
+ <div class="mb-3">
1579
+ <label for="floatingCreationFilterRange" class="form-label">Date Range</label>
1580
+ <input type="text"
1581
+ id="floatingCreationFilterRange"
1582
+ name="creation_filter_range"
1583
+ class="form-control"
1584
+ placeholder="Select date range..."
1585
+ readonly>
1586
+ <!-- Hidden inputs for form submission -->
1587
+ <input type="hidden" id="creation_filter_start" name="creation_filter_start" value="<%= @creation_filter_start %>">
1588
+ <input type="hidden" id="creation_filter_end" name="creation_filter_end" value="<%= @creation_filter_end %>">
1589
+ </div>
1590
+
1591
+ <!-- Quick preset buttons -->
1592
+ <div class="mb-3">
1593
+ <label class="form-label">Quick Presets</label>
1594
+ <div class="d-grid gap-1">
1595
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="lastminute">
1596
+ <i class="bi bi-clock me-1"></i>Last Minute
1597
+ </button>
1598
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last5minutes">
1599
+ <i class="bi bi-clock-history me-1"></i>Last 5 Minutes
1600
+ </button>
1601
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="today">
1602
+ <i class="bi bi-calendar-day me-1"></i>Today
1603
+ </button>
1604
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="yesterday">
1605
+ <i class="bi bi-calendar-minus me-1"></i>Yesterday
1606
+ </button>
1607
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last7days">
1608
+ <i class="bi bi-calendar-week me-1"></i>Last 7 Days
1609
+ </button>
1610
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last30days">
1611
+ <i class="bi bi-calendar-month me-1"></i>Last 30 Days
1612
+ </button>
1613
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="thismonth">
1614
+ <i class="bi bi-calendar3 me-1"></i>This Month
1615
+ </button>
1616
+ </div>
1617
+ </div>
1618
+
1619
+ <div class="d-grid gap-2">
1620
+ <button type="submit" class="btn btn-primary">
1621
+ <i class="bi bi-funnel me-1"></i>Apply Filter
1622
+ </button>
1623
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1624
+ <%
1625
+ # Preserve other query params when clearing creation filter
1626
+ clear_params = {
1627
+ clear_creation_filter: true,
1628
+ page: @current_page,
1629
+ per_page: @per_page,
1630
+ order_by: @order_by,
1631
+ order_direction: @order_direction
1632
+ }
1633
+ # Add column filters except created_at ones
1634
+ @column_filters.each do |key, value|
1635
+ unless key.to_s.start_with?('created_at')
1636
+ clear_params["column_filters[#{key}]"] = value
1637
+ end
1638
+ end
1639
+ %>
1640
+ <a href="<%= request.path %>?<%= clear_params.to_query %>" class="btn btn-outline-secondary">
1641
+ <i class="bi bi-x-circle me-1"></i>Clear Filter
1642
+ </a>
1643
+ <% end %>
1644
+ </div>
1645
+
1646
+ <div class="mt-3">
1647
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1648
+ <div class="alert alert-success alert-sm">
1649
+ <i class="bi bi-check-circle-fill me-1"></i>
1650
+ <strong>Filter Active:</strong> Showing records
1651
+ <% if @creation_filter_start.present? && @creation_filter_end.present? %>
1652
+ between <%= DateTime.parse(@creation_filter_start).strftime("%b %d, %Y %I:%M %p") %>
1653
+ and <%= DateTime.parse(@creation_filter_end).strftime("%b %d, %Y %I:%M %p") %>
1654
+ <% elsif @creation_filter_start.present? %>
1655
+ from <%= DateTime.parse(@creation_filter_start).strftime("%b %d, %Y %I:%M %p") %> onwards
1656
+ <% elsif @creation_filter_end.present? %>
1657
+ up to <%= DateTime.parse(@creation_filter_end).strftime("%b %d, %Y %I:%M %p") %>
1658
+ <% end %>
1659
+
1660
+ <% if @current_page == 1 && @records && @records.rows && @records.rows.empty? %>
1661
+ <div class="mt-2 text-warning">
1662
+ <i class="bi bi-exclamation-triangle-fill me-1"></i>
1663
+ No records match the current filter criteria.
1664
+ </div>
1665
+ <% end %>
1666
+ </div>
1667
+ <% end %>
1668
+ </div>
1669
+ </form>
1670
+ </div>
1671
+ </div>
1672
+
1673
+ <style>
1674
+ /* Floating creation filter button */
1675
+ .floating-creation-filter {
1676
+ position: fixed;
1677
+ bottom: 30px;
1678
+ right: 30px;
1679
+ z-index: 1050;
1680
+ }
1681
+
1682
+ .floating-filter-btn {
1683
+ width: 60px;
1684
+ height: 60px;
1685
+ border-radius: 50%;
1686
+ display: flex;
1687
+ align-items: center;
1688
+ justify-content: center;
1689
+ font-size: 1.2rem;
1690
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1691
+ border: none;
1692
+ position: relative;
1693
+ background: var(--bs-primary);
1694
+ color: white;
1695
+ box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.4);
1696
+ }
1697
+
1698
+ .floating-filter-btn:hover {
1699
+ transform: translateY(-3px) scale(1.05);
1700
+ box-shadow: 0 8px 25px rgba(var(--bs-primary-rgb), 0.5) !important;
1701
+ background: var(--bs-primary) !important;
1702
+ color: white !important;
1703
+ }
1704
+
1705
+ .floating-filter-btn:active {
1706
+ transform: translateY(-1px) scale(1.02);
1707
+ transition: all 0.1s ease;
1708
+ }
1709
+
1710
+ .floating-filter-btn:focus {
1711
+ outline: 2px solid rgba(var(--bs-primary-rgb), 0.5);
1712
+ outline-offset: 2px;
1713
+ }
1714
+
1715
+ /* Badge for active filter indicator */
1716
+ .floating-filter-btn .badge {
1717
+ font-size: 0.6rem;
1718
+ width: 18px;
1719
+ height: 18px;
1720
+ display: flex;
1721
+ align-items: center;
1722
+ justify-content: center;
1723
+ background: var(--bs-success) !important;
1724
+ animation: pulse 2s infinite;
1725
+ }
1726
+
1727
+ @keyframes pulse {
1728
+ 0% { transform: scale(1); }
1729
+ 50% { transform: scale(1.1); }
1730
+ 100% { transform: scale(1); }
1731
+ }
1732
+
1733
+ /* Better datetime input styling for the floating filter */
1734
+ #creationFilterOffcanvas .form-control {
1735
+ border-radius: 6px;
1736
+ border: 1px solid var(--bs-border-color);
1737
+ background-color: var(--bs-body-bg);
1738
+ color: var(--bs-body-color);
1739
+ transition: all 0.15s ease-in-out;
1740
+ }
1741
+
1742
+ #creationFilterOffcanvas .form-control:focus {
1743
+ border-color: var(--bs-primary);
1744
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1745
+ background-color: var(--bs-body-bg);
1746
+ }
1747
+
1748
+ /* Offcanvas enhancements */
1749
+ #creationFilterOffcanvas {
1750
+ backdrop-filter: blur(10px);
1751
+ }
1752
+
1753
+ #creationFilterOffcanvas .offcanvas-header {
1754
+ background: var(--bs-body-bg);
1755
+ border-bottom: 1px solid var(--bs-border-color);
1756
+ padding: 1.25rem;
1757
+ }
1758
+
1759
+ #creationFilterOffcanvas .offcanvas-body {
1760
+ background: var(--bs-body-bg);
1761
+ padding: 1.25rem;
1762
+ }
1763
+
1764
+ #creationFilterOffcanvas .offcanvas-title {
1765
+ color: var(--bs-body-color);
1766
+ font-weight: 600;
1767
+ }
1768
+
1769
+ /* Dark mode specific enhancements */
1770
+ [data-bs-theme="dark"] .floating-filter-btn {
1771
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.2);
1772
+ }
1773
+
1774
+ [data-bs-theme="dark"] .floating-filter-btn:hover {
1775
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.3) !important;
1776
+ }
1777
+
1778
+ [data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-header {
1779
+ background: var(--bs-dark);
1780
+ border-bottom-color: var(--bs-border-color-translucent);
1781
+ }
1782
+
1783
+ [data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-body {
1784
+ background: var(--bs-dark);
1785
+ }
1786
+
1787
+ [data-bs-theme="dark"] #creationFilterOffcanvas .form-control {
1788
+ background-color: var(--bs-body-bg);
1789
+ border-color: var(--bs-border-color-translucent);
1790
+ }
1791
+
1792
+ [data-bs-theme="dark"] #creationFilterOffcanvas .form-control:focus {
1793
+ background-color: var(--bs-body-bg);
1794
+ border-color: var(--bs-primary);
1795
+ }
1796
+
1797
+ /* Date range picker styling */
1798
+ #floatingCreationFilterRange {
1799
+ cursor: pointer;
1800
+ background-color: var(--bs-body-bg);
1801
+ color: var(--bs-body-color);
1802
+ border: 1px solid var(--bs-border-color);
1803
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
1804
+ }
1805
+
1806
+ #floatingCreationFilterRange:focus {
1807
+ border-color: var(--bs-primary);
1808
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1809
+ background-color: var(--bs-body-bg);
1810
+ }
1811
+
1812
+ #floatingCreationFilterRange::placeholder {
1813
+ color: var(--bs-secondary-color);
1814
+ opacity: 0.7;
1815
+ }
1816
+
1817
+ /* Enhanced dark mode support for input */
1818
+ [data-bs-theme="dark"] #floatingCreationFilterRange {
1819
+ background-color: var(--bs-body-bg);
1820
+ border-color: var(--bs-border-color-translucent);
1821
+ color: var(--bs-body-color);
1822
+ }
1823
+
1824
+ [data-bs-theme="dark"] #floatingCreationFilterRange:focus {
1825
+ background-color: var(--bs-body-bg);
1826
+ border-color: var(--bs-primary);
1827
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1828
+ }
1829
+
1830
+ /* Preset buttons styling */
1831
+ .preset-btn {
1832
+ font-size: 0.8rem;
1833
+ padding: 0.35rem 0.75rem;
1834
+ text-align: left;
1835
+ justify-content: flex-start;
1836
+ border: 1px solid var(--bs-border-color);
1837
+ background-color: var(--bs-body-bg);
1838
+ color: var(--bs-body-color);
1839
+ transition: all 0.2s ease;
1840
+ position: relative;
1841
+ overflow: hidden;
1842
+ }
1843
+
1844
+ .preset-btn::before {
1845
+ content: '';
1846
+ position: absolute;
1847
+ top: 0;
1848
+ left: -100%;
1849
+ width: 100%;
1850
+ height: 100%;
1851
+ background: linear-gradient(90deg, transparent, rgba(var(--bs-primary-rgb), 0.1), transparent);
1852
+ transition: left 0.3s ease;
1853
+ }
1854
+
1855
+ .preset-btn:hover {
1856
+ background-color: var(--bs-primary);
1857
+ color: white;
1858
+ border-color: var(--bs-primary);
1859
+ transform: translateY(-1px);
1860
+ box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.2);
1861
+ }
1862
+
1863
+ .preset-btn:hover::before {
1864
+ left: 100%;
1865
+ }
1866
+
1867
+ .preset-btn:active {
1868
+ transform: translateY(0);
1869
+ box-shadow: 0 1px 2px rgba(var(--bs-primary-rgb), 0.2);
1870
+ }
1871
+
1872
+ .preset-btn i {
1873
+ opacity: 0.8;
1874
+ transition: opacity 0.2s ease;
1875
+ }
1876
+
1877
+ .preset-btn:hover i {
1878
+ opacity: 1;
1879
+ }
1880
+
1881
+ /* Dark mode enhancements for preset buttons */
1882
+ [data-bs-theme="dark"] .preset-btn {
1883
+ background-color: var(--bs-dark);
1884
+ border-color: var(--bs-border-color-translucent);
1885
+ color: var(--bs-body-color);
1886
+ }
1887
+
1888
+ [data-bs-theme="dark"] .preset-btn:hover {
1889
+ background-color: var(--bs-primary);
1890
+ color: white;
1891
+ border-color: var(--bs-primary);
1892
+ box-shadow: 0 2px 8px rgba(var(--bs-primary-rgb), 0.3);
1893
+ }
1894
+
1895
+ /* Flatpickr theme adjustments */
1896
+ .flatpickr-calendar {
1897
+ border-radius: 8px;
1898
+ box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.2);
1899
+ border: 1px solid var(--bs-border-color);
1900
+ background: var(--bs-body-bg);
1901
+ font-family: var(--bs-body-font-family);
1902
+ }
1903
+
1904
+ .flatpickr-months {
1905
+ background: var(--bs-body-bg);
1906
+ border-bottom: 1px solid var(--bs-border-color);
1907
+ border-radius: 8px 8px 0 0;
1908
+ }
1909
+
1910
+ .flatpickr-month {
1911
+ color: var(--bs-body-color);
1912
+ fill: var(--bs-body-color);
1913
+ }
1914
+
1915
+ .flatpickr-current-month {
1916
+ color: var(--bs-body-color);
1917
+ }
1918
+
1919
+ .flatpickr-current-month .flatpickr-monthDropdown-month {
1920
+ background: var(--bs-body-bg);
1921
+ color: var(--bs-body-color);
1922
+ }
1923
+
1924
+ .flatpickr-weekdays {
1925
+ background: var(--bs-body-bg);
1926
+ }
1927
+
1928
+ .flatpickr-weekday {
1929
+ color: var(--bs-secondary-color);
1930
+ font-weight: 600;
1931
+ font-size: 0.75rem;
1932
+ }
1933
+
1934
+ .flatpickr-day {
1935
+ color: var(--bs-body-color);
1936
+ border-radius: 4px;
1937
+ transition: all 0.2s ease;
1938
+ }
1939
+
1940
+ .flatpickr-day:hover {
1941
+ background: var(--bs-primary-bg-subtle);
1942
+ color: var(--bs-primary);
1943
+ border-color: var(--bs-primary-border-subtle);
1944
+ }
1945
+
1946
+ .flatpickr-day.selected {
1947
+ background: var(--bs-primary);
1948
+ color: white;
1949
+ border-color: var(--bs-primary);
1950
+ box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.3);
1951
+ }
1952
+
1953
+ .flatpickr-day.selected:hover {
1954
+ background: var(--bs-primary);
1955
+ color: white;
1956
+ }
1957
+
1958
+ .flatpickr-day.inRange {
1959
+ background: var(--bs-primary-bg-subtle);
1960
+ color: var(--bs-primary);
1961
+ border-color: transparent;
1962
+ }
1963
+
1964
+ .flatpickr-day.startRange {
1965
+ background: var(--bs-primary);
1966
+ color: white;
1967
+ border-radius: 4px 0 0 4px;
1968
+ }
1969
+
1970
+ .flatpickr-day.endRange {
1971
+ background: var(--bs-primary);
1972
+ color: white;
1973
+ border-radius: 0 4px 4px 0;
1974
+ }
1975
+
1976
+ .flatpickr-day.startRange.endRange {
1977
+ border-radius: 4px;
1978
+ }
1979
+
1980
+ .flatpickr-day.today {
1981
+ border-color: var(--bs-primary);
1982
+ color: var(--bs-primary);
1983
+ font-weight: 600;
1984
+ }
1985
+
1986
+ .flatpickr-day.today:hover {
1987
+ background: var(--bs-primary);
1988
+ color: white;
1989
+ }
1990
+
1991
+ .flatpickr-day.disabled {
1992
+ color: var(--bs-secondary-color);
1993
+ opacity: 0.5;
1994
+ }
1995
+
1996
+ .flatpickr-time {
1997
+ background: var(--bs-body-bg);
1998
+ border-radius: 0 0 8px 8px;
1999
+ }
2000
+
2001
+ .flatpickr-time input {
2002
+ background: var(--bs-body-bg);
2003
+ color: var(--bs-body-color);
2004
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
2005
+ }
2006
+
2007
+ .flatpickr-time input:focus {
2008
+ border-color: var(--bs-primary);
2009
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
2010
+ outline: 0;
2011
+ }
2012
+
2013
+ .flatpickr-time .flatpickr-time-separator {
2014
+ color: var(--bs-body-color);
2015
+ }
2016
+
2017
+ .flatpickr-prev-month,
2018
+ .flatpickr-next-month {
2019
+ color: var(--bs-body-color);
2020
+ fill: var(--bs-body-color);
2021
+ transition: color 0.2s ease;
2022
+ }
2023
+
2024
+ .flatpickr-prev-month:hover,
2025
+ .flatpickr-next-month:hover {
2026
+ color: var(--bs-primary);
2027
+ fill: var(--bs-primary);
2028
+ }
2029
+
2030
+ /* Dark mode specific enhancements */
2031
+ [data-bs-theme="dark"] .flatpickr-calendar {
2032
+ background: var(--bs-dark);
2033
+ border-color: var(--bs-border-color-translucent);
2034
+ box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4);
2035
+ }
2036
+
2037
+ [data-bs-theme="dark"] .flatpickr-months {
2038
+ background: var(--bs-dark);
2039
+ border-bottom-color: var(--bs-border-color-translucent);
2040
+ }
2041
+
2042
+ [data-bs-theme="dark"] .flatpickr-weekdays {
2043
+ background: var(--bs-dark);
2044
+ }
2045
+
2046
+ /* Enhanced dark mode day styling for better contrast */
2047
+ [data-bs-theme="dark"] .flatpickr-day {
2048
+ color: #e9ecef !important;
2049
+ background: transparent;
2050
+ border: 1px solid transparent;
2051
+ }
2052
+
2053
+ [data-bs-theme="dark"] .flatpickr-day:hover {
2054
+ background: rgba(var(--bs-primary-rgb), 0.25) !important;
2055
+ color: #ffffff !important;
2056
+ border-color: rgba(var(--bs-primary-rgb), 0.4);
2057
+ }
2058
+
2059
+ [data-bs-theme="dark"] .flatpickr-day.inRange {
2060
+ background: rgba(var(--bs-primary-rgb), 0.2) !important;
2061
+ color: #ffffff !important;
2062
+ border-color: transparent;
2063
+ }
2064
+
2065
+ [data-bs-theme="dark"] .flatpickr-day.selected {
2066
+ background: var(--bs-primary) !important;
2067
+ color: #ffffff !important;
2068
+ border-color: var(--bs-primary);
2069
+ box-shadow: 0 2px 6px rgba(var(--bs-primary-rgb), 0.4);
2070
+ }
2071
+
2072
+ [data-bs-theme="dark"] .flatpickr-day.selected:hover {
2073
+ background: var(--bs-primary) !important;
2074
+ color: #ffffff !important;
2075
+ }
2076
+
2077
+ [data-bs-theme="dark"] .flatpickr-day.startRange {
2078
+ background: var(--bs-primary) !important;
2079
+ color: #ffffff !important;
2080
+ border-radius: 4px 0 0 4px;
2081
+ }
2082
+
2083
+ [data-bs-theme="dark"] .flatpickr-day.endRange {
2084
+ background: var(--bs-primary) !important;
2085
+ color: #ffffff !important;
2086
+ border-radius: 0 4px 4px 0;
2087
+ }
2088
+
2089
+ [data-bs-theme="dark"] .flatpickr-day.startRange.endRange {
2090
+ border-radius: 4px;
2091
+ }
2092
+
2093
+ [data-bs-theme="dark"] .flatpickr-day.today {
2094
+ border-color: var(--bs-primary) !important;
2095
+ color: var(--bs-primary) !important;
2096
+ font-weight: 600;
2097
+ background: rgba(var(--bs-primary-rgb), 0.1);
2098
+ }
2099
+
2100
+ [data-bs-theme="dark"] .flatpickr-day.today:hover {
2101
+ background: var(--bs-primary) !important;
2102
+ color: #ffffff !important;
2103
+ }
2104
+
2105
+ [data-bs-theme="dark"] .flatpickr-day.disabled {
2106
+ color: #6c757d !important;
2107
+ opacity: 0.4;
2108
+ background: transparent !important;
2109
+ }
2110
+
2111
+ [data-bs-theme="dark"] .flatpickr-day.disabled:hover {
2112
+ background: transparent !important;
2113
+ color: #6c757d !important;
2114
+ cursor: not-allowed;
2115
+ }
2116
+
2117
+ /* Dark mode other day states */
2118
+ [data-bs-theme="dark"] .flatpickr-day.nextMonthDay,
2119
+ [data-bs-theme="dark"] .flatpickr-day.prevMonthDay {
2120
+ color: #6c757d !important;
2121
+ opacity: 0.6;
2122
+ }
2123
+
2124
+ [data-bs-theme="dark"] .flatpickr-day.nextMonthDay:hover,
2125
+ [data-bs-theme="dark"] .flatpickr-day.prevMonthDay:hover {
2126
+ background: rgba(var(--bs-primary-rgb), 0.15) !important;
2127
+ color: #adb5bd !important;
2128
+ }
2129
+
2130
+ [data-bs-theme="dark"] .flatpickr-time {
2131
+ background: var(--bs-dark);
2132
+ border-top-color: var(--bs-border-color-translucent);
2133
+ }
2134
+
2135
+ [data-bs-theme="dark"] .flatpickr-time input {
2136
+ background: var(--bs-body-bg);
2137
+ border-color: var(--bs-border-color-translucent);
2138
+ color: #e9ecef !important;
2139
+ }
2140
+
2141
+ .flatpickr-calendar.hasTime .flatpickr-time {
2142
+ border-top: var(--bs-border-color-translucent);
2143
+ }
2144
+
2145
+ .flatpickr-next-month {
2146
+ color: var(--bs-body-color);
2147
+ fill: var(--bs-body-color);
2148
+ }
2149
+
2150
+ span.flatpickr-weekday {
2151
+ color: var(--bs-secondary-color);
2152
+ font-weight: 600;
2153
+ font-size: 0.75rem;
2154
+ }
2155
+
2156
+ [data-bs-theme="dark"] .flatpickr-time input:focus {
2157
+ border-color: var(--bs-primary);
2158
+ background: var(--bs-body-bg);
2159
+ color: #ffffff !important;
2160
+ }
2161
+
2162
+ [data-bs-theme="dark"] .flatpickr-time .flatpickr-time-separator {
2163
+ color: #e9ecef !important;
2164
+ }
2165
+
2166
+ /* Better focus states */
2167
+ .flatpickr-day:focus {
2168
+ outline: 2px solid var(--bs-primary);
2169
+ outline-offset: -2px;
2170
+ z-index: 10;
2171
+ }
2172
+
2173
+ /* Animation for calendar appearance */
2174
+ .flatpickr-calendar.open {
2175
+ animation: flatpickr-slideDown 0.2s ease-out;
2176
+ }
2177
+
2178
+ @keyframes flatpickr-slideDown {
2179
+ from {
2180
+ opacity: 0;
2181
+ transform: translateY(-10px) scale(0.98);
2182
+ }
2183
+ to {
2184
+ opacity: 1;
2185
+ transform: translateY(0) scale(1);
2186
+ }
2187
+ }
2188
+
2189
+ /* Small alert styling */
2190
+ .alert-sm {
2191
+ padding: 0.5rem;
2192
+ font-size: 0.875rem;
2193
+ }
2194
+
2195
+ /* Responsive adjustments - hide on smaller screens */
2196
+ @media (max-width: 991.98px) {
2197
+ .floating-creation-filter {
2198
+ display: none !important;
2199
+ }
2200
+ }
2201
+
2202
+ /* Flatpickr positioning and responsive adjustments */
2203
+ .flatpickr-calendar.arrowTop:before,
2204
+ .flatpickr-calendar.arrowTop:after {
2205
+ border-bottom-color: var(--bs-border-color);
2206
+ }
2207
+
2208
+ .flatpickr-calendar.arrowBottom:before,
2209
+ .flatpickr-calendar.arrowBottom:after {
2210
+ border-top-color: var(--bs-border-color);
2211
+ }
2212
+
2213
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowTop:before,
2214
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowTop:after {
2215
+ border-bottom-color: var(--bs-border-color-translucent);
2216
+ }
2217
+
2218
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:before,
2219
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:after {
2220
+ border-top-color: var(--bs-border-color-translucent);
2221
+ }
2222
+
2223
+ /* Ensure calendar stays within viewport on mobile */
2224
+ @media (max-width: 576px) {
2225
+ .flatpickr-calendar {
2226
+ max-width: calc(100vw - 20px);
2227
+ font-size: 14px;
2228
+ }
2229
+
2230
+ .flatpickr-day {
2231
+ height: 35px;
2232
+ line-height: 35px;
2233
+ }
2234
+
2235
+ .flatpickr-time input {
2236
+ font-size: 14px;
2237
+ }
2238
+ }
2239
+
2240
+ /* Improved accessibility */
2241
+ .flatpickr-calendar {
2242
+ font-family: inherit;
2243
+ }
2244
+
2245
+ .flatpickr-day[aria-label] {
2246
+ position: relative;
2247
+ }
2248
+
2249
+ /* Smooth scroll behavior for time inputs */
2250
+ .flatpickr-time input {
2251
+ scroll-behavior: smooth;
2252
+ }
2253
+
2254
+ /* Enhanced visual feedback for interactive elements */
2255
+ .flatpickr-prev-month,
2256
+ .flatpickr-next-month {
2257
+ border-radius: 4px;
2258
+ padding: 4px;
2259
+ margin: 2px;
2260
+ }
2261
+
2262
+ .flatpickr-prev-month:hover,
2263
+ .flatpickr-next-month:hover {
2264
+ background: rgba(var(--bs-primary-rgb), 0.1);
2265
+ }
2266
+ </style>
2267
+
2268
+ <script>
2269
+ document.addEventListener('DOMContentLoaded', function() {
2270
+ // Initialize Flatpickr date range picker
2271
+ const dateRangeInput = document.getElementById('floatingCreationFilterRange');
2272
+ const startHidden = document.getElementById('creation_filter_start');
2273
+ const endHidden = document.getElementById('creation_filter_end');
2274
+
2275
+ if (dateRangeInput && typeof flatpickr !== 'undefined') {
2276
+ console.log('Flatpickr library loaded, initializing date range picker');
2277
+ // Store the Flatpickr instance in a variable accessible to all handlers
2278
+ let fp;
2279
+
2280
+ // Function to initialize Flatpickr
2281
+ function initializeFlatpickr(theme) {
2282
+ // Determine theme based on current document theme or passed parameter
2283
+ const currentTheme = theme || (document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light');
2284
+
2285
+ const config = {
2286
+ mode: 'range',
2287
+ enableTime: true,
2288
+ dateFormat: 'Y-m-d H:i',
2289
+ time_24hr: true,
2290
+ allowInput: false,
2291
+ clickOpens: true,
2292
+ theme: currentTheme,
2293
+ animate: true,
2294
+ position: 'auto',
2295
+ static: false,
2296
+ appendTo: document.body, // Ensure it renders above other elements
2297
+ locale: {
2298
+ rangeSeparator: ' to ',
2299
+ weekdays: {
2300
+ shorthand: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
2301
+ longhand: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
2302
+ },
2303
+ months: {
2304
+ shorthand: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
2305
+ longhand: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
2306
+ }
2307
+ },
2308
+ onOpen: function(selectedDates, dateStr, instance) {
2309
+ // Add a slight delay to apply theme-specific styling after calendar opens
2310
+ setTimeout(() => {
2311
+ const calendar = instance.calendarContainer;
2312
+ if (calendar) {
2313
+ // Apply theme-specific class for additional styling control
2314
+ calendar.classList.add(`flatpickr-${currentTheme}`);
2315
+
2316
+ // Ensure proper z-index for offcanvas overlay
2317
+ calendar.style.zIndex = '1070';
2318
+
2319
+ // Add elegant entrance animation
2320
+ calendar.classList.add('open');
2321
+ }
2322
+ }, 10);
2323
+ },
2324
+ onClose: function(selectedDates, dateStr, instance) {
2325
+ const calendar = instance.calendarContainer;
2326
+ if (calendar) {
2327
+ calendar.classList.remove('open');
2328
+ }
2329
+ },
2330
+ onChange: function(selectedDates, dateStr, instance) {
2331
+ console.log('Date range changed:', selectedDates);
2332
+
2333
+ if (selectedDates.length === 2) {
2334
+ // Format dates for hidden inputs (Rails expects ISO format)
2335
+ startHidden.value = selectedDates[0].toISOString().slice(0, 16);
2336
+ endHidden.value = selectedDates[1].toISOString().slice(0, 16);
2337
+
2338
+ // Update display with elegant formatting
2339
+ const formatOptions = {
2340
+ year: 'numeric',
2341
+ month: 'short',
2342
+ day: 'numeric',
2343
+ hour: '2-digit',
2344
+ minute: '2-digit',
2345
+ hour12: false
2346
+ };
2347
+
2348
+ const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
2349
+ const endFormatted = selectedDates[1].toLocaleDateString('en-US', formatOptions);
2350
+ dateRangeInput.value = `${startFormatted} to ${endFormatted}`;
2351
+
2352
+ } else if (selectedDates.length === 1) {
2353
+ startHidden.value = selectedDates[0].toISOString().slice(0, 16);
2354
+ endHidden.value = '';
2355
+
2356
+ const formatOptions = {
2357
+ year: 'numeric',
2358
+ month: 'short',
2359
+ day: 'numeric',
2360
+ hour: '2-digit',
2361
+ minute: '2-digit',
2362
+ hour12: false
2363
+ };
2364
+
2365
+ const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
2366
+ dateRangeInput.value = `${startFormatted} (select end date)`;
2367
+
2368
+ } else {
2369
+ startHidden.value = '';
2370
+ endHidden.value = '';
2371
+ dateRangeInput.value = '';
2372
+ }
2373
+ }
2374
+ };
2375
+
2376
+ return flatpickr(dateRangeInput, config);
2377
+ }
2378
+
2379
+ // Initialize date range picker
2380
+ fp = initializeFlatpickr();
2381
+
2382
+ // Set initial values if they exist
2383
+ if (startHidden.value || endHidden.value) {
2384
+ const dates = [];
2385
+ if (startHidden.value) {
2386
+ dates.push(new Date(startHidden.value));
2387
+ }
2388
+ if (endHidden.value) {
2389
+ dates.push(new Date(endHidden.value));
2390
+ }
2391
+ fp.setDate(dates);
2392
+ }
2393
+
2394
+ // Preset button functionality
2395
+ const presetButtons = document.querySelectorAll('.preset-btn');
2396
+ presetButtons.forEach(button => {
2397
+ button.addEventListener('click', function(event) {
2398
+ event.preventDefault(); // Prevent any form submission
2399
+
2400
+ const preset = this.getAttribute('data-preset');
2401
+ const now = new Date();
2402
+ let startDate, endDate;
2403
+
2404
+ console.log('Preset button clicked:', preset); // Debug log
2405
+
2406
+ switch (preset) {
2407
+ case 'lastminute':
2408
+ startDate = new Date(now);
2409
+ startDate.setMinutes(startDate.getMinutes() - 1);
2410
+ endDate = new Date(now);
2411
+ break;
2412
+ case 'last5minutes':
2413
+ startDate = new Date(now);
2414
+ startDate.setMinutes(startDate.getMinutes() - 5);
2415
+ endDate = new Date(now);
2416
+ break;
2417
+ case 'today':
2418
+ startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
2419
+ endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
2420
+ break;
2421
+ case 'yesterday':
2422
+ const yesterday = new Date(now);
2423
+ yesterday.setDate(yesterday.getDate() - 1);
2424
+ startDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 0, 0, 0);
2425
+ endDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 23, 59, 59);
2426
+ break;
2427
+ case 'last7days':
2428
+ startDate = new Date(now);
2429
+ startDate.setDate(startDate.getDate() - 7);
2430
+ startDate.setHours(0, 0, 0, 0);
2431
+ endDate = new Date(now);
2432
+ endDate.setHours(23, 59, 59, 999);
2433
+ break;
2434
+ case 'last30days':
2435
+ startDate = new Date(now);
2436
+ startDate.setDate(startDate.getDate() - 30);
2437
+ startDate.setHours(0, 0, 0, 0);
2438
+ endDate = new Date(now);
2439
+ endDate.setHours(23, 59, 59, 999);
2440
+ break;
2441
+ case 'thismonth':
2442
+ startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
2443
+ endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
2444
+ break;
2445
+ }
2446
+
2447
+ if (startDate && endDate && fp) {
2448
+ console.log('Setting dates:', startDate, endDate); // Debug log
2449
+ fp.setDate([startDate, endDate]);
2450
+
2451
+ // Also update the hidden inputs directly as a fallback
2452
+ startHidden.value = startDate.toISOString().slice(0, 16);
2453
+ endHidden.value = endDate.toISOString().slice(0, 16);
2454
+
2455
+ // Update the display value
2456
+ const formattedStart = startDate.toLocaleDateString() + ' ' + startDate.toLocaleTimeString();
2457
+ const formattedEnd = endDate.toLocaleDateString() + ' ' + endDate.toLocaleTimeString();
2458
+ dateRangeInput.value = formattedStart + ' to ' + formattedEnd;
2459
+ } else {
2460
+ console.error('Failed to set dates - startDate:', startDate, 'endDate:', endDate, 'fp:', fp);
2461
+ }
2462
+ });
2463
+ });
2464
+
2465
+ // Listen for theme changes and update Flatpickr theme
2466
+ document.addEventListener('dbviewerThemeChanged', function(e) {
2467
+ const newTheme = e.detail.theme === 'dark' ? 'dark' : 'light';
2468
+ console.log('Theme changed to:', newTheme);
2469
+
2470
+ // Destroy and recreate with new theme
2471
+ if (fp) {
2472
+ const currentDates = fp.selectedDates;
2473
+ fp.destroy();
2474
+ fp = initializeFlatpickr(newTheme);
2475
+
2476
+ // Restore previous values if they existed
2477
+ if (currentDates && currentDates.length > 0) {
2478
+ fp.setDate(currentDates);
2479
+ }
2480
+ }
2481
+ });
2482
+
2483
+ // Also listen for direct data-bs-theme attribute changes using MutationObserver
2484
+ const themeObserver = new MutationObserver(function(mutations) {
2485
+ mutations.forEach(function(mutation) {
2486
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-bs-theme') {
2487
+ const newTheme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light';
2488
+ console.log('Theme attribute changed to:', newTheme);
2489
+
2490
+ if (fp) {
2491
+ const currentDates = fp.selectedDates;
2492
+ fp.destroy();
2493
+ fp = initializeFlatpickr(newTheme);
2494
+
2495
+ // Restore previous values if they existed
2496
+ if (currentDates && currentDates.length > 0) {
2497
+ fp.setDate(currentDates);
2498
+ }
2499
+ }
2500
+ }
2501
+ });
2502
+ });
2503
+
2504
+ // Start observing theme changes
2505
+ themeObserver.observe(document.documentElement, {
2506
+ attributes: true,
2507
+ attributeFilter: ['data-bs-theme']
2508
+ });
2509
+ } else {
2510
+ console.error('Date range picker initialization failed:', {
2511
+ dateRangeInput: !!dateRangeInput,
2512
+ flatpickr: typeof flatpickr !== 'undefined'
2513
+ });
2514
+ }
2515
+
2516
+ // Close offcanvas after form submission
2517
+ const form = document.getElementById('floatingCreationFilterForm');
2518
+ if (form) {
2519
+ form.addEventListener('submit', function() {
2520
+ const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('creationFilterOffcanvas'));
2521
+ if (offcanvas) {
2522
+ setTimeout(() => {
2523
+ offcanvas.hide();
2524
+ }, 100);
2525
+ }
2526
+ });
2527
+ }
2528
+ });
2529
+ </script>
2530
+ <% end %>
@@ -23,6 +23,8 @@
23
23
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
24
24
  <!-- Bootstrap Icons -->
25
25
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
26
+ <!-- Flatpickr CSS for Date Range Picker -->
27
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
26
28
  <!-- Google Fonts -->
27
29
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
28
30
  <!-- Chart.js for Data Visualization -->
@@ -1407,6 +1409,8 @@
1407
1409
 
1408
1410
  <!-- Bootstrap JS -->
1409
1411
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
1412
+ <!-- Flatpickr JS for Date Range Picker -->
1413
+ <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
1410
1414
 
1411
1415
  <!-- Dark Mode Toggle Script -->
1412
1416
  <script>
@@ -4,95 +4,7 @@
4
4
  id="tableSearch" placeholder="Filter tables..." aria-label="Filter tables">
5
5
  </div>
6
6
 
7
- <div class="p-2">
8
- <div class="accordion accordion-flush" id="creationFilterAccordion">
9
- <div class="accordion-item border-0">
10
- <h2 class="accordion-header" id="creationFilterHeading">
11
- <button class="accordion-button p-2 collapsed" type="button" data-bs-toggle="collapse"
12
- data-bs-target="#creationFilterCollapse" aria-expanded="false"
13
- aria-controls="creationFilterCollapse">
14
- <i class="bi bi-calendar-range me-2"></i> Creation Filter
15
- <% if @creation_filter_start.present? || @creation_filter_end.present? %>
16
- <% if @table_name.present? && has_timestamp_column?(@table_name) %>
17
- <span class="badge bg-success ms-2">Active</span>
18
- <% else %>
19
- <span class="badge bg-secondary ms-2">Set</span>
20
- <% end %>
21
- <% end %>
22
- </button>
23
- </h2>
24
- <div id="creationFilterCollapse" class="accordion-collapse collapse" aria-labelledby="creationFilterHeading">
25
- <div class="accordion-body p-2">
26
- <form id="creationFilterForm" action="<%= request.path %>" method="get" class="mb-0">
27
- <!-- Preserve existing query parameters -->
28
- <input type="hidden" name="page" value="<%= @current_page %>">
29
- <input type="hidden" name="per_page" value="<%= @per_page %>">
30
- <input type="hidden" name="order_by" value="<%= @order_by %>">
31
- <input type="hidden" name="order_direction" value="<%= @order_direction %>">
32
-
33
- <!-- Datetime range fields -->
34
- <div class="mb-2">
35
- <label for="creationFilterStart" class="form-label mb-1 small">Start Date/Time</label>
36
- <input type="datetime-local" id="creationFilterStart" name="creation_filter_start"
37
- class="form-control form-control-sm" value="<%= @creation_filter_start %>">
38
- </div>
39
- <div class="mb-2">
40
- <label for="creationFilterEnd" class="form-label mb-1 small">End Date/Time</label>
41
- <input type="datetime-local" id="creationFilterEnd" name="creation_filter_end"
42
- class="form-control form-control-sm" value="<%= @creation_filter_end %>">
43
- </div>
44
-
45
- <div class="d-flex justify-content-between">
46
- <button type="submit" class="btn btn-primary btn-sm">Apply</button>
47
- <% if @creation_filter_start.present? || @creation_filter_end.present? %>
48
- <%
49
- # Preserve other query params when clearing creation filter
50
- clear_params = {
51
- clear_creation_filter: true,
52
- page: @current_page,
53
- per_page: @per_page,
54
- order_by: @order_by,
55
- order_direction: @order_direction
56
- }
57
- %>
58
- <a href="<%= request.path %>?<%= clear_params.to_query %>" class="btn btn-outline-secondary btn-sm">Clear</a>
59
- <% end %>
60
- </div>
61
- <div class="mt-2 small">
62
- <% if @table_name.present? && has_timestamp_column?(@table_name) && (@creation_filter_start.present? || @creation_filter_end.present?) %>
63
- <div class="text-success">
64
- <i class="bi bi-check-circle-fill"></i>
65
- Filter active on this table.
66
- <% if @current_page == 1 && @records && @records.rows && @records.rows.empty? %>
67
- <div class="alert alert-warning p-1 mt-2 small">
68
- <i class="bi bi-exclamation-triangle-fill"></i>
69
- No records match the filter criteria.
70
- </div>
71
- <% end %>
72
- </div>
73
- <% elsif @table_name.present? && (@creation_filter_start.present? || @creation_filter_end.present?) %>
74
- <div class="text-warning">
75
- <i class="bi bi-exclamation-circle-fill"></i>
76
- This table has no <code>created_at</code> column.
77
- </div>
78
- <% elsif !@table_name.present? && (@creation_filter_start.present? || @creation_filter_end.present?) %>
79
- <div class="text-info">
80
- <i class="bi bi-info-circle-fill"></i>
81
- Filter will be applied on tables with <code>created_at</code> column.
82
- </div>
83
- <% else %>
84
- <div class="text-muted">
85
- <i class="bi bi-info-circle-fill"></i>
86
- Filters apply to tables with a <code>created_at</code> column.
87
- </div>
88
- <% end %>
89
- </div>
90
- </form>
91
- </div>
92
- </div>
93
- </div>
94
- </div>
95
- </div>
7
+
96
8
 
97
9
  <!-- Add custom styling for datetime inputs -->
98
10
  <style>
@@ -199,9 +111,21 @@
199
111
  <span id="table-count"><%= @tables.size %></span> tables found
200
112
  </div>
201
113
 
114
+ <div class="px-3 py-1 text-muted small border-top">
115
+ <i class="bi bi-gem me-1"></i>
116
+ DBViewer v<%= Dbviewer::VERSION %>
117
+ </div>
118
+
202
119
  <script>
203
120
  document.addEventListener('DOMContentLoaded', function() {
204
121
  const searchInput = document.getElementById('tableSearch');
122
+ const sidebarContent = document.querySelector('.dbviewer-sidebar-content');
123
+
124
+ // Storage keys for persistence
125
+ const STORAGE_KEYS = {
126
+ searchFilter: 'dbviewer_sidebar_search_filter',
127
+ scrollPosition: 'dbviewer_sidebar_scroll_position'
128
+ };
205
129
 
206
130
  if (searchInput) {
207
131
  // Debounce function to limit how often the filter runs
@@ -223,6 +147,9 @@
223
147
  const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
224
148
  let visibleCount = 0;
225
149
 
150
+ // Save the current search filter to localStorage
151
+ localStorage.setItem(STORAGE_KEYS.searchFilter, searchInput.value);
152
+
226
153
  tableItems.forEach(function(item) {
227
154
  // Get the table name from the title attribute for more accurate matching
228
155
  const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
@@ -261,26 +188,7 @@
261
188
  }
262
189
  }, 150); // Debounce for 150ms
263
190
 
264
- // Set up event listeners for the search input
265
- searchInput.addEventListener('input', filterTables);
266
- searchInput.addEventListener('keyup', function(e) {
267
- filterTables();
268
-
269
- // Add keyboard navigation for the filtered list
270
- if (e.key === 'Enter' || e.key === 'ArrowDown') {
271
- e.preventDefault();
272
- // Focus the first visible table item (not having d-none class)
273
- const firstVisibleItem = document.querySelector('#tablesList .list-group-item-action:not(.d-none)');
274
- if (firstVisibleItem) {
275
- firstVisibleItem.focus();
276
- // Make sure the item is visible in the scrollable area
277
- firstVisibleItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
278
- }
279
- }
280
- });
281
- searchInput.addEventListener('search', filterTables); // For clearing via the "x" in some browsers
282
-
283
- // Clear the search box when clicking the X button
191
+ // Set up clear button first
284
192
  const clearButton = document.createElement('button');
285
193
  clearButton.type = 'button';
286
194
  clearButton.className = 'btn btn-sm btn-link position-absolute';
@@ -296,6 +204,8 @@
296
204
  clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
297
205
  clearButton.addEventListener('click', function() {
298
206
  searchInput.value = '';
207
+ // Clear the saved filter from localStorage
208
+ localStorage.removeItem(STORAGE_KEYS.searchFilter);
299
209
  // Call filter directly without debouncing for immediate feedback
300
210
  filterTables();
301
211
  this.style.display = 'none';
@@ -309,13 +219,90 @@
309
219
  searchInput.addEventListener('input', function() {
310
220
  clearButton.style.display = this.value ? 'block' : 'none';
311
221
  });
222
+ }
223
+
224
+ // Restore saved search filter on page load and apply it immediately
225
+ const savedFilter = localStorage.getItem(STORAGE_KEYS.searchFilter);
226
+ if (savedFilter) {
227
+ searchInput.value = savedFilter;
228
+ // Show clear button immediately when filter is restored
229
+ clearButton.style.display = 'block';
230
+ // Apply filter immediately without debouncing to prevent blinking
231
+ const query = savedFilter.toLowerCase();
232
+ const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
233
+ let visibleCount = 0;
234
+
235
+ tableItems.forEach(function(item) {
236
+ const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
237
+ const displayedText = item.textContent.trim().toLowerCase();
238
+
239
+ if (tableName.includes(query) || displayedText.includes(query)) {
240
+ item.classList.remove('d-none');
241
+ visibleCount++;
242
+ } else {
243
+ item.classList.add('d-none');
244
+ }
245
+ });
312
246
 
313
- // Apply filter initially in case there's a value already (e.g., from browser autofill)
314
- if (searchInput.value) {
315
- filterTables();
316
- clearButton.style.display = 'block';
247
+ // Update the tables count immediately
248
+ const tableCountElement = document.getElementById('table-count');
249
+ if (tableCountElement) {
250
+ tableCountElement.textContent = visibleCount;
251
+ }
252
+
253
+ // Handle no results message immediately
254
+ let noResultsEl = document.getElementById('dbviewer-no-filter-results');
255
+ if (visibleCount === 0 && query !== '') {
256
+ if (!noResultsEl) {
257
+ noResultsEl = document.createElement('div');
258
+ noResultsEl.id = 'dbviewer-no-filter-results';
259
+ noResultsEl.className = 'list-group-item text-muted text-center py-3';
260
+ noResultsEl.innerHTML = '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
261
+ document.getElementById('tablesList').appendChild(noResultsEl);
262
+ }
263
+ noResultsEl.querySelector('.fw-bold').textContent = query;
264
+ noResultsEl.style.display = 'block';
265
+ } else if (noResultsEl) {
266
+ noResultsEl.style.display = 'none';
267
+ }
268
+ }
269
+
270
+ // Restore saved scroll position on page load
271
+ if (sidebarContent) {
272
+ const savedScrollPosition = localStorage.getItem(STORAGE_KEYS.scrollPosition);
273
+ if (savedScrollPosition) {
274
+ // Use requestAnimationFrame to ensure DOM is fully rendered
275
+ requestAnimationFrame(() => {
276
+ sidebarContent.scrollTop = parseInt(savedScrollPosition, 10);
277
+ });
317
278
  }
279
+
280
+ // Save scroll position on scroll
281
+ const saveScrollPosition = debounce(function() {
282
+ localStorage.setItem(STORAGE_KEYS.scrollPosition, sidebarContent.scrollTop);
283
+ }, 100);
284
+
285
+ sidebarContent.addEventListener('scroll', saveScrollPosition);
318
286
  }
287
+
288
+ // Set up event listeners for the search input
289
+ searchInput.addEventListener('input', filterTables);
290
+ searchInput.addEventListener('keyup', function(e) {
291
+ filterTables();
292
+
293
+ // Add keyboard navigation for the filtered list
294
+ if (e.key === 'Enter' || e.key === 'ArrowDown') {
295
+ e.preventDefault();
296
+ // Focus the first visible table item (not having d-none class)
297
+ const firstVisibleItem = document.querySelector('#tablesList .list-group-item-action:not(.d-none)');
298
+ if (firstVisibleItem) {
299
+ firstVisibleItem.focus();
300
+ // Make sure the item is visible in the scrollable area
301
+ firstVisibleItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
302
+ }
303
+ }
304
+ });
305
+ searchInput.addEventListener('search', filterTables); // For clearing via the "x" in some browsers
319
306
  }
320
307
  });
321
308
  </script>
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.5.5"
2
+ VERSION = "0.5.7"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.5
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh