@redpanda-data/docs-extensions-and-macros 4.7.0 β†’ 4.7.2

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.
@@ -1,5 +1,4 @@
1
1
  'use strict';
2
-
3
2
  /**
4
3
  * Registers macros for use in Redpanda Connect contexts in the Redpanda documentation.
5
4
  * @param {Registry} registry - The Antora registry where this block macro is registered.
@@ -7,23 +6,31 @@
7
6
  */
8
7
  module.exports.register = function (registry, context) {
9
8
  function filterComponentTable() {
10
- const nameInput = document.getElementById('componentTableSearch').value.trim().toLowerCase();
11
- const typeFilter = Array.from(document.querySelector('#typeFilter').selectedOptions).map(option => option.value);
12
- const cloudSupportInput= document.getElementById('cloudSupportFilter')?.value;
13
-
9
+ const nameInputElement = document.getElementById('componentTableSearch');
10
+ const nameInput = nameInputElement ? nameInputElement.value.trim().toLowerCase() : '';
11
+ const typeFilter = Array.from(document.querySelectorAll('#typeFilterMenu input[type="checkbox"]:checked')).map(checkbox => checkbox.value);
14
12
  // Check for the existence of support and enterprise license filters (optional)
15
- const supportFilterElement = document.querySelector('#supportFilter');
13
+ const supportFilterElement = document.querySelector('#supportFilterMenu');
16
14
  const supportFilter = supportFilterElement
17
- ? Array.from(supportFilterElement.selectedOptions).map(option => option.value)
15
+ ? Array.from(supportFilterElement.querySelectorAll('input[type="checkbox"]:checked')).map(checkbox => checkbox.value)
16
+ : [];
17
+ // Check for cloud support filter (optional)
18
+ const cloudSupportFilterElement = document.querySelector('#cloudSupportFilterMenu');
19
+ const cloudSupportFilter = cloudSupportFilterElement
20
+ ? Array.from(cloudSupportFilterElement.querySelectorAll('input[type="checkbox"]:checked')).map(checkbox => checkbox.value)
21
+ : [];
22
+ // Check for enterprise license filter (optional)
23
+ const enterpriseFilterElement = document.querySelector('#enterpriseFilterMenu');
24
+ const enterpriseFilter = enterpriseFilterElement
25
+ ? Array.from(enterpriseFilterElement.querySelectorAll('input[type="checkbox"]:checked')).map(checkbox => checkbox.value)
18
26
  : [];
19
-
20
27
  const params = getQueryParams();
21
28
  const enterpriseSupportFilter = params.support === 'enterprise'; // Check if 'support=enterprise' is in the URL
22
- const cloudSupportFilter = params.support === 'cloud'; // Check if 'support=cloud' is in the URL
23
-
29
+ const cloudSupportFilterFromUrl = params.support === 'cloud'; // Check if 'support=cloud' is in the URL
24
30
  const table = document.getElementById('componentTable');
31
+ if (!table) return; // Exit early if table doesn't exist
25
32
  const trs = table.getElementsByTagName('tr');
26
-
33
+ if (!trs || trs.length === 0) return; // Exit early if no rows found
27
34
  for (let i = 1; i < trs.length; i++) {
28
35
  const row = trs[i];
29
36
  const nameTd = row.querySelector('td[id^="componentName-"]');
@@ -31,32 +38,96 @@ module.exports.register = function (registry, context) {
31
38
  const supportTd = row.querySelector('td[id^="componentSupport-"]'); // Support column, if present
32
39
  const enterpriseSupportTd = row.querySelector('td[id^="componentLicense-"]'); // Enterprise License column, if present
33
40
  const cloudSupportTd = row.querySelector('td[id^="componentCloud-"]'); // Cloud support column, if present
34
-
35
41
  if (typeTd) { // Ensure that at least the Type column is present
36
42
  const nameText = nameTd ? nameTd.textContent.trim().toLowerCase() : '';
37
43
  const typeText = typeTd.textContent.trim().toLowerCase().split(', ').map(item => item.trim());
38
44
  const supportText = supportTd ? supportTd.textContent.trim().toLowerCase() : '';
39
45
  const enterpriseSupportText = enterpriseSupportTd ? enterpriseSupportTd.textContent.trim().toLowerCase() : ''; // Yes or No
40
46
  const cloudSupportText = cloudSupportTd ? cloudSupportTd.textContent.trim().toLowerCase() : ''; // Yes or No
41
-
47
+ // Check cloud support filter
48
+ let cloudSupportMatch = true;
49
+ if (cloudSupportFilter.length > 0 && !cloudSupportFilter.includes('')) {
50
+ // If specific options are selected (not "All")
51
+ cloudSupportMatch = cloudSupportFilter.some(value => {
52
+ if (value === 'yes') return cloudSupportText === 'yes' || cloudSupportText.includes('yes');
53
+ if (value === 'no') return cloudSupportText === 'no' || !cloudSupportText.includes('yes');
54
+ return true;
55
+ });
56
+ }
57
+ // Check enterprise license filter
58
+ let enterpriseLicenseMatch = true;
59
+ if (enterpriseFilter.length > 0 && !enterpriseFilter.includes('')) {
60
+ // If specific options are selected (not "All")
61
+ enterpriseLicenseMatch = enterpriseFilter.some(value => {
62
+ if (value === 'yes') return enterpriseSupportText === 'yes' || enterpriseSupportText.includes('yes');
63
+ if (value === 'no') return enterpriseSupportText === 'no' || !enterpriseSupportText.includes('yes');
64
+ return true;
65
+ });
66
+ }
42
67
  // Determine if the row should be shown
43
68
  const showRow =
44
69
  ((!nameInput || nameText.includes(nameInput)) && // Filter by name if present
45
70
  (typeFilter.length === 0 || typeFilter.some(value => typeText.includes(value))) && // Filter by type
46
71
  (!supportTd || supportFilter.length === 0 || supportFilter.some(value => supportText.includes(value))) && // Filter by support if present
47
- (!enterpriseSupportFilter || !enterpriseSupportTd || supportText.includes('enterprise') || enterpriseSupportText === 'yes') // Filter by enterprise support if 'support=enterprise' is in the URL
48
- &&
49
- (!cloudSupportFilter || !cloudSupportTd || supportText.includes('cloud') || cloudSupportText === 'yes') && // Filter by cloud support if 'support=cloud' is in the URL
50
- (!cloudSupportInput || cloudSupportText === cloudSupportInput)
72
+ (!enterpriseSupportFilter || !enterpriseSupportTd || supportText.includes('enterprise') || enterpriseSupportText === 'yes') && // Filter by enterprise support if 'support=enterprise' is in the URL
73
+ (!cloudSupportFilterFromUrl || !cloudSupportTd || supportText.includes('cloud') || cloudSupportText === 'yes') && // Filter by cloud support if 'support=cloud' is in the URL
74
+ cloudSupportMatch && // Filter by cloud support dropdown
75
+ enterpriseLicenseMatch // Filter by enterprise license dropdown
51
76
  );
52
-
53
77
  row.style.display = showRow ? '' : 'none';
54
78
  } else {
55
79
  row.style.display = 'none'; // Hide row if the Type column is missing
56
80
  }
57
81
  }
82
+ // Update dropdown text based on selections
83
+ updateDropdownText('typeFilter', 'All Types Selected', 'Types Selected');
84
+ const supportMenu = document.getElementById('supportFilterMenu');
85
+ if (supportMenu) {
86
+ updateDropdownText('supportFilter', 'All Support Levels Selected', 'Support Levels Selected');
87
+ }
88
+ const cloudSupportMenu = document.getElementById('cloudSupportFilterMenu');
89
+ if (cloudSupportMenu) {
90
+ updateDropdownText('cloudSupportFilter', 'All Options Selected', 'Options Selected');
91
+ }
92
+ const enterpriseMenu = document.getElementById('enterpriseFilterMenu');
93
+ if (enterpriseMenu) {
94
+ updateDropdownText('enterpriseFilter', 'All Options Selected', 'Options Selected');
95
+ }
96
+ // Update URL parameters based on current filter selections
97
+ updateURLParameters();
98
+ }
99
+ function updateURLParameters() {
100
+ const params = new URLSearchParams();
101
+ // Get current filter values
102
+ const nameInputElement = document.getElementById('componentTableSearch');
103
+ const nameInput = nameInputElement ? nameInputElement.value.trim() : '';
104
+ const typeFilter = Array.from(document.querySelectorAll('#typeFilterMenu input[type="checkbox"]:checked')).map(checkbox => checkbox.value);
105
+ const supportFilterElement = document.querySelector('#supportFilterMenu');
106
+ const supportFilter = supportFilterElement
107
+ ? Array.from(supportFilterElement.querySelectorAll('input[type="checkbox"]:checked')).map(checkbox => checkbox.value)
108
+ : [];
109
+ const cloudSupportFilterElement = document.querySelector('#cloudSupportFilterMenu');
110
+ const cloudSupportFilter = cloudSupportFilterElement
111
+ ? Array.from(cloudSupportFilterElement.querySelectorAll('input[type="checkbox"]:checked')).map(checkbox => checkbox.value)
112
+ : [];
113
+ const enterpriseFilterElement = document.querySelector('#enterpriseFilterMenu');
114
+ const enterpriseFilter = enterpriseFilterElement
115
+ ? Array.from(enterpriseFilterElement.querySelectorAll('input[type="checkbox"]:checked')).map(checkbox => checkbox.value)
116
+ : [];
117
+ // Add parameters to URL if they have values
118
+ if (nameInput) params.set('search', nameInput);
119
+ if (typeFilter.length > 0) params.set('type', typeFilter.join(','));
120
+ if (supportFilter.length > 0) params.set('support', supportFilter.join(','));
121
+ if (cloudSupportFilter.length > 0 && !cloudSupportFilter.includes('')) {
122
+ params.set('cloud', cloudSupportFilter.join(','));
123
+ }
124
+ if (enterpriseFilter.length > 0 && !enterpriseFilter.includes('')) {
125
+ params.set('enterprise', enterpriseFilter.join(','));
126
+ }
127
+ // Update the URL without refreshing the page
128
+ const newURL = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
129
+ window.history.replaceState({}, '', newURL);
58
130
  }
59
-
60
131
  /**
61
132
  * Gets the first URL (either Redpanda Connect or Redpanda Cloud) for a given connector from the typesArray.
62
133
  * If the cloud option is enabled (`isCloud = true`), it prefers the Redpanda Cloud URL; otherwise, it returns the Redpanda Connect URL.
@@ -76,12 +147,10 @@ module.exports.register = function (registry, context) {
76
147
  if (isCloud && redpandaCloudUrl) {
77
148
  return redpandaCloudUrl;
78
149
  }
79
-
80
150
  // Return Connect URL if isCloud is false or no Cloud URL exists
81
151
  if (!isCloud && redpandaConnectUrl) {
82
152
  return redpandaConnectUrl;
83
153
  }
84
-
85
154
  // If Cloud URL exists but isCloud is false, fallback to Cloud URL if no Connect URL exists
86
155
  if (!isCloud && redpandaCloudUrl) {
87
156
  return redpandaCloudUrl;
@@ -90,7 +159,6 @@ module.exports.register = function (registry, context) {
90
159
  }
91
160
  return ''; // Return an empty string if no URL is found
92
161
  }
93
-
94
162
  const capitalize = s => s && s[0].toUpperCase() + s.slice(1);
95
163
 
96
164
  /**
@@ -285,7 +353,6 @@ module.exports.register = function (registry, context) {
285
353
  function generateConnectorsHTMLTable(connectors, sqlDrivers, isCloud, showAllInfo) {
286
354
  return Object.entries(connectors)
287
355
  .filter(([_, details]) => {
288
-
289
356
  // If isCloud is true, filter out rows that do not support cloud
290
357
  return !isCloud || details.isCloudConnectorSupported;
291
358
  })
@@ -518,41 +585,94 @@ module.exports.register = function (registry, context) {
518
585
  if (row.support_level) uniqueSupportLevel.add(row.support_level);
519
586
  });
520
587
 
521
- const createOptions = (values) =>
588
+ const createDropdownCheckboxOptions = (values, id) =>
522
589
  Array.from(values)
523
- .map(value => `<option selected value="${value}">${capitalize(value).replace("_", " ")}</option>`)
590
+ .map(value => `
591
+ <label class="dropdown-checkbox-option">
592
+ <input type="checkbox" value="${value}" checked onchange="filterComponentTable()">
593
+ <span>${capitalize(value).replace("_", " ")}</span>
594
+ </label>`)
524
595
  .join('');
525
596
 
526
597
  let tableHtml = `
527
598
  <div class="table-filters">
528
599
  <input class="table-search" type="text" id="componentTableSearch" onkeyup="filterComponentTable()" placeholder="Search for components...">
529
- <label for="typeFilter">Type:</label>
530
- <select multiple class="type-dropdown" id="typeFilter" onchange="filterComponentTable()">
531
- ${createOptions(types)}
532
- </select>
600
+ <div class="filter-group">
601
+ <label for="typeFilterToggle">Type:</label>
602
+ <div class="dropdown-checkbox-wrapper">
603
+ <button type="button" class="dropdown-checkbox-toggle" id="typeFilterToggle" onclick="toggleDropdownCheckbox('typeFilter')" aria-expanded="false" aria-haspopup="true" aria-controls="typeFilterMenu">
604
+ <span class="dropdown-text">All Types Selected</span>
605
+ <span class="dropdown-arrow">β–Ό</span>
606
+ </button>
607
+ <div class="dropdown-checkbox-menu" id="typeFilterMenu" role="menu" aria-labelledby="typeFilterToggle">
608
+ ${createDropdownCheckboxOptions(types, 'typeFilter')}
609
+ </div>
610
+ </div>
611
+ </div>
533
612
  `;
534
613
 
535
614
  if (!isCloud) {
536
615
  tableHtml += `
537
- <br><label for="supportFilter" id="labelForSupportFilter">Support:</label>
538
- <select multiple class="type-dropdown" id="supportFilter" onchange="filterComponentTable()">
539
- ${createOptions(uniqueSupportLevel)}
540
- </select>
616
+ <div class="filter-group">
617
+ <label for="supportFilterToggle" id="labelForSupportFilter">Support:</label>
618
+ <div class="dropdown-checkbox-wrapper">
619
+ <button type="button" class="dropdown-checkbox-toggle" id="supportFilterToggle" onclick="toggleDropdownCheckbox('supportFilter')" aria-expanded="false" aria-haspopup="true" aria-controls="supportFilterMenu">
620
+ <span class="dropdown-text">All Support Levels Selected</span>
621
+ <span class="dropdown-arrow">β–Ό</span>
622
+ </button>
623
+ <div class="dropdown-checkbox-menu" id="supportFilterMenu" role="menu" aria-labelledby="supportFilterToggle">
624
+ ${createDropdownCheckboxOptions(uniqueSupportLevel, 'supportFilter')}
625
+ </div>
626
+ </div>
627
+ </div>
541
628
  `;
542
629
  }
543
630
 
544
631
  if (showAllInfo) {
545
632
  tableHtml += `
546
- <br><label for="cloudSupportFilter">Available in Cloud:</label>
547
- <select class="type-dropdown" id="cloudSupportFilter" onchange="filterComponentTable()">
548
- <option value="">All</option>
549
- <option value="yes">Yes</option>
550
- <option value="no">No</option>
551
- </select>
633
+ <div class="filter-group">
634
+ <label for="cloudSupportFilterToggle">Available in Cloud:</label>
635
+ <div class="dropdown-checkbox-wrapper">
636
+ <button type="button" class="dropdown-checkbox-toggle" id="cloudSupportFilterToggle" onclick="toggleDropdownCheckbox('cloudSupportFilter')" aria-expanded="false" aria-haspopup="true" aria-controls="cloudSupportFilterMenu">
637
+ <span class="dropdown-text">All Options Selected</span>
638
+ <span class="dropdown-arrow">β–Ό</span>
639
+ </button>
640
+ <div class="dropdown-checkbox-menu" id="cloudSupportFilterMenu" role="menu" aria-labelledby="cloudSupportFilterToggle">
641
+ <label class="dropdown-checkbox-option">
642
+ <input type="checkbox" value="yes" checked onchange="filterComponentTable()">
643
+ <span>Yes</span>
644
+ </label>
645
+ <label class="dropdown-checkbox-option">
646
+ <input type="checkbox" value="no" checked onchange="filterComponentTable()">
647
+ <span>No</span>
648
+ </label>
649
+ </div>
650
+ </div>
651
+ </div>
652
+ <div class="filter-group">
653
+ <label for="enterpriseFilterToggle">Enterprise License:</label>
654
+ <div class="dropdown-checkbox-wrapper">
655
+ <button type="button" class="dropdown-checkbox-toggle" id="enterpriseFilterToggle" onclick="toggleDropdownCheckbox('enterpriseFilter')" aria-expanded="false" aria-haspopup="true" aria-controls="enterpriseFilterMenu">
656
+ <span class="dropdown-text">All Options Selected</span>
657
+ <span class="dropdown-arrow">β–Ό</span>
658
+ </button>
659
+ <div class="dropdown-checkbox-menu" id="enterpriseFilterMenu" role="menu" aria-labelledby="enterpriseFilterToggle">
660
+ <label class="dropdown-checkbox-option">
661
+ <input type="checkbox" value="yes" checked onchange="filterComponentTable()">
662
+ <span>Yes</span>
663
+ </label>
664
+ <label class="dropdown-checkbox-option">
665
+ <input type="checkbox" value="no" checked onchange="filterComponentTable()">
666
+ <span>No</span>
667
+ </label>
668
+ </div>
669
+ </div>
670
+ </div>
552
671
  `;
553
672
  }
554
673
 
555
674
  tableHtml += `</div>
675
+ <!-- CSS styles are defined in the external redpanda-connect-filters.css stylesheet -->
556
676
  <table class="tableblock frame-all grid-all stripes-even no-clip stretch component-table sortable" id="componentTable">
557
677
  <colgroup>
558
678
  ${showAllInfo
@@ -580,6 +700,7 @@ module.exports.register = function (registry, context) {
580
700
  </table>
581
701
  <script>
582
702
  ${filterComponentTable.toString()}
703
+ ${updateURLParameters.toString()}
583
704
  function getQueryParams() {
584
705
  const params = {};
585
706
  const searchParams = new URLSearchParams(window.location.search);
@@ -589,30 +710,223 @@ module.exports.register = function (registry, context) {
589
710
  return params;
590
711
  }
591
712
 
592
- // Initialize Choices.js for type dropdowns
713
+ // Define global dropdown functions (shared between macros)
714
+ window.initializeDropdownFunctions = window.initializeDropdownFunctions || function() {
715
+ // Component type dropdown toggle function
716
+ window.toggleComponentTypeDropdown = function() {
717
+ const toggle = document.getElementById('componentTypeDropdownToggle');
718
+ const menu = document.getElementById('componentTypeDropdownMenu');
719
+
720
+ if (!toggle || !menu) return;
721
+
722
+ const isOpen = menu.classList.contains('show');
723
+
724
+ // Close all other dropdowns first (including filter dropdowns)
725
+ document.querySelectorAll('.dropdown-checkbox-menu.show, .dropdown-menu.show').forEach(dropdown => {
726
+ if (dropdown !== menu) {
727
+ dropdown.classList.remove('show');
728
+ const otherToggle = dropdown.parentNode.querySelector('.dropdown-checkbox-toggle, .dropdown-toggle');
729
+ if (otherToggle) {
730
+ otherToggle.classList.remove('open');
731
+ otherToggle.setAttribute('aria-expanded', 'false');
732
+ }
733
+ }
734
+ });
735
+
736
+ // Toggle current dropdown
737
+ if (isOpen) {
738
+ menu.classList.remove('show');
739
+ toggle.classList.remove('open');
740
+ toggle.setAttribute('aria-expanded', 'false');
741
+ } else {
742
+ menu.classList.add('show');
743
+ toggle.classList.add('open');
744
+ toggle.setAttribute('aria-expanded', 'true');
745
+ // Focus first option
746
+ const firstOption = menu.querySelector('.dropdown-option');
747
+ if (firstOption) firstOption.focus();
748
+ }
749
+ };
750
+
751
+ };
752
+
753
+ // Initialize the functions
754
+ window.initializeDropdownFunctions();
755
+
756
+ function toggleDropdownCheckbox(filterId) {
757
+ const toggle = document.getElementById(filterId + 'Toggle');
758
+ const menu = document.getElementById(filterId + 'Menu');
759
+
760
+ if (!toggle || !menu) return;
761
+
762
+ const isOpen = menu.classList.contains('show');
763
+
764
+ // Close all other dropdowns first
765
+ document.querySelectorAll('.dropdown-checkbox-menu.show').forEach(dropdown => {
766
+ if (dropdown !== menu) {
767
+ dropdown.classList.remove('show');
768
+ const otherToggle = dropdown.parentNode.querySelector('.dropdown-checkbox-toggle');
769
+ if (otherToggle) {
770
+ otherToggle.classList.remove('open');
771
+ otherToggle.setAttribute('aria-expanded', 'false');
772
+ }
773
+ }
774
+ });
775
+
776
+ // Toggle current dropdown
777
+ if (isOpen) {
778
+ menu.classList.remove('show');
779
+ toggle.classList.remove('open');
780
+ toggle.setAttribute('aria-expanded', 'false');
781
+ } else {
782
+ menu.classList.add('show');
783
+ toggle.classList.add('open');
784
+ toggle.setAttribute('aria-expanded', 'true');
785
+ }
786
+ }
787
+
788
+ function updateDropdownText(filterId, allSelectedText, someSelectedText) {
789
+ const menu = document.getElementById(filterId + 'Menu');
790
+ const toggle = document.getElementById(filterId + 'Toggle');
791
+
792
+ if (!menu || !toggle) return;
793
+
794
+ const checkboxes = menu.querySelectorAll('input[type="checkbox"]');
795
+ const checkedCount = menu.querySelectorAll('input[type="checkbox"]:checked').length;
796
+ const totalCount = checkboxes.length;
797
+ const textElement = toggle.querySelector('.dropdown-text');
798
+
799
+ if (!textElement) return;
800
+
801
+ if (checkedCount === 0) {
802
+ textElement.textContent = 'None Selected';
803
+ } else if (checkedCount === totalCount) {
804
+ textElement.textContent = allSelectedText;
805
+ } else if (checkedCount === 1) {
806
+ const checkedBox = menu.querySelector('input[type="checkbox"]:checked');
807
+ if (checkedBox) {
808
+ const label = checkedBox.nextElementSibling;
809
+ textElement.textContent = label ? label.textContent : getSingularText(someSelectedText);
810
+ }
811
+ } else {
812
+ textElement.textContent = checkedCount + ' ' + someSelectedText;
813
+ }
814
+ }
815
+
816
+ function getSingularText(pluralText) {
817
+ // Handle various plural patterns and convert to singular
818
+ if (pluralText.includes('Types Selected')) {
819
+ return 'Type Selected';
820
+ } else if (pluralText.includes('Support Levels Selected')) {
821
+ return 'Support Level Selected';
822
+ } else if (pluralText.includes('Options Selected')) {
823
+ return 'Option Selected';
824
+ } else if (pluralText.includes('Items Selected')) {
825
+ return 'Item Selected';
826
+ } else if (pluralText.includes('Categories Selected')) {
827
+ return 'Category Selected';
828
+ } else if (pluralText.includes('Filters Selected')) {
829
+ return 'Filter Selected';
830
+ } else if (pluralText.endsWith('s Selected')) {
831
+ // Generic fallback for words ending in 's Selected'
832
+ return pluralText.replace(/s Selected$/, ' Selected');
833
+ } else if (pluralText.endsWith('ies Selected')) {
834
+ // Handle words ending in 'ies' (e.g., "Categories Selected" -> "Category Selected")
835
+ return pluralText.replace(/ies Selected$/, 'y Selected');
836
+ } else {
837
+ // If no pattern matches, return as-is
838
+ return pluralText;
839
+ }
840
+ }
841
+
842
+ // Close dropdown when clicking outside (local handler for filter dropdowns only)
843
+ document.addEventListener('click', function(event) {
844
+ if (!event.target.closest('.dropdown-checkbox-wrapper')) {
845
+ document.querySelectorAll('.dropdown-checkbox-menu.show').forEach(menu => {
846
+ menu.classList.remove('show');
847
+ const toggle = menu.parentNode.querySelector('.dropdown-checkbox-toggle');
848
+ if (toggle) {
849
+ toggle.classList.remove('open');
850
+ toggle.setAttribute('aria-expanded', 'false');
851
+ }
852
+ });
853
+ }
854
+ });
855
+
856
+ // Add keyboard navigation support (local handler for filter dropdowns only)
857
+ document.addEventListener('keydown', function(event) {
858
+ if (event.key === 'Escape') {
859
+ // Close all open filter dropdowns on Escape
860
+ document.querySelectorAll('.dropdown-checkbox-menu.show').forEach(menu => {
861
+ menu.classList.remove('show');
862
+ const toggle = menu.parentNode.querySelector('.dropdown-checkbox-toggle');
863
+ if (toggle) {
864
+ toggle.classList.remove('open');
865
+ toggle.setAttribute('aria-expanded', 'false');
866
+ toggle.focus(); // Return focus to toggle button
867
+ }
868
+ });
869
+ }
870
+ });
871
+
872
+ // Initialize filters from URL parameters
593
873
  document.addEventListener('DOMContentLoaded', function() {
594
874
  const params = getQueryParams();
595
875
  const search = document.getElementById('componentTableSearch');
596
- const typeFilter = document.getElementById('typeFilter');
597
- const supportFilter = document.getElementById('supportFilter');
876
+ const typeFilterMenu = document.getElementById('typeFilterMenu');
877
+ const supportFilterMenu = document.getElementById('supportFilterMenu');
878
+ const cloudSupportFilterMenu = document.getElementById('cloudSupportFilterMenu');
879
+ const enterpriseFilterMenu = document.getElementById('enterpriseFilterMenu');
880
+
598
881
  if (params.search && search) {
599
882
  search.value = params.search;
600
883
  }
601
- if (params.type && typeFilter) {
602
- typeFilter.value = params.type;
884
+
885
+ if (params.type && typeFilterMenu) {
886
+ const types = params.type.split(',');
887
+ // First uncheck all checkboxes
888
+ typeFilterMenu.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
889
+ // Then check only the ones in the URL
890
+ types.forEach(type => {
891
+ const checkbox = typeFilterMenu.querySelector(\`input[value="\${type}"]\`);
892
+ if (checkbox) checkbox.checked = true;
893
+ });
603
894
  }
604
- if (params.support && supportFilter) {
605
- supportFilter.value = params.support;
895
+
896
+ if (params.support && supportFilterMenu) {
897
+ const supports = params.support.split(',');
898
+ // First uncheck all checkboxes
899
+ supportFilterMenu.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
900
+ // Then check only the ones in the URL
901
+ supports.forEach(support => {
902
+ const checkbox = supportFilterMenu.querySelector(\`input[value="\${support}"]\`);
903
+ if (checkbox) checkbox.checked = true;
904
+ });
606
905
  }
607
- filterComponentTable();
608
- const typeDropdowns = document.querySelectorAll('.type-dropdown');
609
- typeDropdowns.forEach(dropdown => {
610
- new Choices(dropdown, {
611
- searchEnabled: false,
612
- allowHTML: true,
613
- removeItemButton: true
906
+
907
+ if (params.cloud && cloudSupportFilterMenu) {
908
+ const cloudOptions = params.cloud.split(',');
909
+ // First uncheck all checkboxes
910
+ cloudSupportFilterMenu.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
911
+ // Then check only the ones in the URL
912
+ cloudOptions.forEach(option => {
913
+ const checkbox = cloudSupportFilterMenu.querySelector(\`input[value="\${option}"]\`);
914
+ if (checkbox) checkbox.checked = true;
614
915
  });
615
- });
916
+ }
917
+
918
+ if (params.enterprise && enterpriseFilterMenu) {
919
+ const enterpriseOptions = params.enterprise.split(',');
920
+ // First uncheck all checkboxes
921
+ enterpriseFilterMenu.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
922
+ // Then check only the ones in the URL
923
+ enterpriseOptions.forEach(option => {
924
+ const checkbox = enterpriseFilterMenu.querySelector(\`input[value="\${option}"]\`);
925
+ if (checkbox) checkbox.checked = true;
926
+ });
927
+ }
928
+
929
+ filterComponentTable();
616
930
  });
617
931
  </script>
618
932
  `;
@@ -737,35 +1051,71 @@ module.exports.register = function (registry, context) {
737
1051
  // Build the dropdown for types with links depending on the current component
738
1052
  let typeDropdown = '';
739
1053
  if (sortedTypes.length > 1) {
740
- const dropdownLinks = sortedTypes.map(typeObj => {
1054
+ const dropdownOptions = sortedTypes.map(typeObj => {
741
1055
  const link = (component === 'Cloud' && typeObj.redpandaCloudUrl) || typeObj.redpandaConnectUrl;
742
- return `<option value="${link}" data-support="${typeObj.support}">${capitalize(typeObj.type)}</option>`;
1056
+ return `<a href="${link}" class="dropdown-option" role="menuitem" tabindex="-1">${capitalize(typeObj.type)}</a>`;
743
1057
  }).join('');
744
1058
  typeDropdown = `
745
- <p style="display: flex;align-items: center;gap: 6px;"><strong>Type:</strong>
746
- <select class="type-dropdown" onchange="window.location.href=this.value">
747
- ${dropdownLinks}
748
- </select>
749
- </p>
750
- <script>
751
- // Initialize Choices.js for type dropdowns
752
- document.addEventListener('DOMContentLoaded', function() {
753
- const typeDropdowns = document.querySelectorAll('.type-dropdown');
754
- typeDropdowns.forEach(dropdown => {
755
- new Choices(dropdown, { searchEnabled: false, allowHTML: true, shouldSort: false, itemSelectText: '' });
756
- });
757
- });
758
- </script>`;
1059
+ <div class="dropdown-wrapper">
1060
+ <p class="type-dropdown-container"><strong>Type:</strong>
1061
+ <button type="button" class="dropdown-toggle" id="componentTypeDropdownToggle" onclick="toggleComponentTypeDropdown()" aria-expanded="false" aria-haspopup="true" aria-controls="componentTypeDropdownMenu">
1062
+ <span class="dropdown-text">${capitalize(sortedTypes[0].type)}</span>
1063
+ <span class="dropdown-arrow">β–Ό</span>
1064
+ </button>
1065
+ <div class="dropdown-menu" id="componentTypeDropdownMenu" role="menu" aria-labelledby="componentTypeDropdownToggle">
1066
+ ${dropdownOptions}
1067
+ </div>
1068
+ </p>
1069
+ </div>`;
759
1070
  }
760
1071
  // Return the metadata block with consistent layout
761
1072
  return self.createBlock(parent, 'pass', `
762
1073
  <div class="metadata-block">
763
- <div style="padding:10px;display: flex;flex-direction: column;gap: 6px;">
1074
+ <div class="metadata-content">
764
1075
  ${typeDropdown}
765
1076
  ${availableInInfo}
766
1077
  ${enterpriseLicenseInfo}
767
1078
  </div>
768
- </div>`);
1079
+ </div>
1080
+ <script>
1081
+ // Define global dropdown functions directly (shared between macros)
1082
+ if (!window.toggleComponentTypeDropdown) {
1083
+ window.toggleComponentTypeDropdown = function() {
1084
+ const toggle = document.getElementById('componentTypeDropdownToggle');
1085
+ const menu = document.getElementById('componentTypeDropdownMenu');
1086
+
1087
+ if (!toggle || !menu) return;
1088
+
1089
+ const isOpen = menu.classList.contains('show');
1090
+
1091
+ // Close all other dropdowns first (including filter dropdowns)
1092
+ document.querySelectorAll('.dropdown-checkbox-menu.show, .dropdown-menu.show').forEach(dropdown => {
1093
+ if (dropdown !== menu) {
1094
+ dropdown.classList.remove('show');
1095
+ const otherToggle = dropdown.parentNode.querySelector('.dropdown-checkbox-toggle, .dropdown-toggle');
1096
+ if (otherToggle) {
1097
+ otherToggle.classList.remove('open');
1098
+ otherToggle.setAttribute('aria-expanded', 'false');
1099
+ }
1100
+ }
1101
+ });
1102
+
1103
+ // Toggle current dropdown
1104
+ if (isOpen) {
1105
+ menu.classList.remove('show');
1106
+ toggle.classList.remove('open');
1107
+ toggle.setAttribute('aria-expanded', 'false');
1108
+ } else {
1109
+ menu.classList.add('show');
1110
+ toggle.classList.add('open');
1111
+ toggle.setAttribute('aria-expanded', 'true');
1112
+ // Focus first option
1113
+ const firstOption = menu.querySelector('.dropdown-option');
1114
+ if (firstOption) firstOption.focus();
1115
+ }
1116
+ };
1117
+ }
1118
+ </script>`);
769
1119
  });
770
1120
  });
771
1121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.7.0",
3
+ "version": "4.7.2",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -90,6 +90,8 @@ generate-docs:
90
90
  @mkdir -p "$(OUTPUT_DIR)"
91
91
  @cd $(TOOL_ROOT) && \
92
92
  $(PYTHON) json-to-asciidoc/generate_docs.py --output-dir "$(OUTPUT_DIR)"
93
+ @echo "πŸ“„ Copying properties-output.json to $(OUTPUT_DIR)…"
94
+ @cp "$(TOOL_ROOT)/gen/properties-output.json" "$(OUTPUT_DIR)/"
93
95
  @echo "βœ… Docs generated at $(OUTPUT_DIR)"
94
96
 
95
97
  # --- Debug helper to print all the key paths/vars ---
@@ -261,6 +261,12 @@ def generate_property_doc(key, value):
261
261
  if prop_type in ["string", "array", "number", "boolean", "integer"]:
262
262
  lines.append(f"*Type:* {prop_type}\n\n")
263
263
 
264
+ # Add aliases if they exist
265
+ aliases = value.get("aliases")
266
+ if aliases and len(aliases) > 0:
267
+ aliases_str = ", ".join(f"`{alias}`" for alias in aliases)
268
+ lines.append(f"*Aliases:* {aliases_str}\n\n")
269
+
264
270
  if value.get("maximum") is not None and value.get("minimum") is not None:
265
271
  lines.append(
266
272
  f"*Accepted values:* [`{value.get('minimum')}`, `{value.get('maximum')}`]\n\n"
@@ -135,14 +135,67 @@ function mergeOverrides(target, overrides) {
135
135
  return target;
136
136
  }
137
137
 
138
+ /**
139
+ * Resolves $ref references in an object by replacing them with their definitions.
140
+ * Supports JSON Pointer style references like "#/definitions/client_certs".
141
+ *
142
+ * @param {Object} obj - The object to resolve references in
143
+ * @param {Object} root - The root object containing definitions
144
+ * @returns {Object} The object with references resolved
145
+ */
146
+ function resolveReferences(obj, root) {
147
+ if (!obj || typeof obj !== 'object') {
148
+ return obj;
149
+ }
150
+
151
+ if (Array.isArray(obj)) {
152
+ return obj.map(item => resolveReferences(item, root));
153
+ }
154
+
155
+ const result = {};
156
+
157
+ for (const [key, value] of Object.entries(obj)) {
158
+ if (key === '$ref' && typeof value === 'string') {
159
+ // Handle JSON Pointer style references
160
+ if (value.startsWith('#/')) {
161
+ const path = value.substring(2).split('/');
162
+ let resolved = root;
163
+
164
+ try {
165
+ for (const segment of path) {
166
+ resolved = resolved[segment];
167
+ }
168
+
169
+ if (resolved === undefined) {
170
+ throw new Error(`Reference path not found: ${value}`);
171
+ }
172
+
173
+ // Merge the resolved object, but don't process $ref in the resolved object
174
+ // to avoid infinite recursion
175
+ Object.assign(result, resolved);
176
+ } catch (err) {
177
+ throw new Error(`Failed to resolve reference "${value}": ${err.message}`);
178
+ }
179
+ } else {
180
+ throw new Error(`Unsupported reference format: ${value}. Only JSON Pointer references starting with '#/' are supported.`);
181
+ }
182
+ } else {
183
+ // Recursively resolve references in nested objects
184
+ result[key] = resolveReferences(value, root);
185
+ }
186
+ }
187
+
188
+ return result;
189
+ }
190
+
138
191
  /**
139
192
  * Generates documentation files for RPCN connectors using Handlebars templates.
140
193
  *
141
- * Depending on the {@link writeFullDrafts} flag, generates either partial documentation files for connector fields and examples, or full draft documentation for each connector component. Supports merging override data and skips draft generation for components marked as deprecated.
194
+ * Depending on the {@link writeFullDrafts} flag, generates either partial documentation files for connector fields and examples, or full draft documentation for each connector component. Supports merging override data with $ref references and skips draft generation for components marked as deprecated.
142
195
  *
143
196
  * @param {Object} options - Configuration options for documentation generation.
144
197
  * @param {string} options.data - Path to the connector data file (JSON or YAML).
145
- * @param {string} [options.overrides] - Optional path to a JSON file with override data.
198
+ * @param {string} [options.overrides] - Optional path to a JSON file with override data. Supports $ref references in JSON Pointer format (e.g., "#/definitions/client_certs").
146
199
  * @param {string} options.template - Path to the main Handlebars template.
147
200
  * @param {string} [options.templateIntro] - Path to the intro partial template (used in full draft mode).
148
201
  * @param {string} [options.templateFields] - Path to the fields partial template.
@@ -150,7 +203,7 @@ function mergeOverrides(target, overrides) {
150
203
  * @param {boolean} options.writeFullDrafts - If true, generates full draft documentation; otherwise, generates partials.
151
204
  * @returns {Promise<Object>} An object summarizing the number and paths of generated partials and drafts.
152
205
  *
153
- * @throws {Error} If reading or parsing input files fails, or if template rendering fails for a component.
206
+ * @throws {Error} If reading or parsing input files fails, if template rendering fails for a component, or if $ref references cannot be resolved.
154
207
  *
155
208
  * @remark
156
209
  * When generating full drafts, components with a `status` of `'deprecated'` are skipped.
@@ -175,7 +228,11 @@ async function generateRpcnConnectorDocs(options) {
175
228
  if (overrides) {
176
229
  const ovRaw = fs.readFileSync(overrides, 'utf8');
177
230
  const ovObj = JSON.parse(ovRaw);
178
- mergeOverrides(dataObj, ovObj);
231
+
232
+ // Resolve any $ref references in the overrides
233
+ const resolvedOverrides = resolveReferences(ovObj, ovObj);
234
+
235
+ mergeOverrides(dataObj, resolvedOverrides);
179
236
  }
180
237
 
181
238
  // Compile the β€œmain” template (used when writeFullDrafts = true)
@@ -285,5 +342,6 @@ async function generateRpcnConnectorDocs(options) {
285
342
 
286
343
  module.exports = {
287
344
  generateRpcnConnectorDocs,
288
- mergeOverrides
345
+ mergeOverrides,
346
+ resolveReferences
289
347
  };