@underpostnet/underpost 2.96.1 → 2.97.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.dockerignore +1 -2
  2. package/.env.development +0 -3
  3. package/.env.production +0 -3
  4. package/.env.test +0 -3
  5. package/.prettierignore +1 -2
  6. package/README.md +31 -31
  7. package/baremetal/commission-workflows.json +94 -17
  8. package/bin/deploy.js +1 -1
  9. package/cli.md +75 -41
  10. package/conf.js +1 -0
  11. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  12. package/manifests/deployment/dd-test-development/deployment.yaml +4 -4
  13. package/package.json +3 -2
  14. package/packer/scripts/fuse-tar-root +3 -3
  15. package/scripts/disk-clean.sh +128 -187
  16. package/scripts/gpu-diag.sh +2 -2
  17. package/scripts/ip-info.sh +11 -11
  18. package/scripts/ipxe-setup.sh +197 -0
  19. package/scripts/maas-upload-boot-resource.sh +1 -1
  20. package/scripts/nvim.sh +1 -1
  21. package/scripts/packer-setup.sh +13 -13
  22. package/scripts/ports-ls.sh +31 -0
  23. package/scripts/quick-tftp.sh +19 -0
  24. package/scripts/rocky-setup.sh +2 -2
  25. package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
  26. package/scripts/ssl.sh +7 -7
  27. package/src/api/document/document.controller.js +15 -0
  28. package/src/api/document/document.model.js +44 -1
  29. package/src/api/document/document.router.js +2 -0
  30. package/src/api/document/document.service.js +398 -26
  31. package/src/cli/baremetal.js +2001 -463
  32. package/src/cli/cloud-init.js +354 -231
  33. package/src/cli/cluster.js +51 -53
  34. package/src/cli/db.js +22 -0
  35. package/src/cli/deploy.js +7 -3
  36. package/src/cli/image.js +1 -0
  37. package/src/cli/index.js +40 -37
  38. package/src/cli/lxd.js +3 -3
  39. package/src/cli/run.js +78 -12
  40. package/src/cli/ssh.js +1 -1
  41. package/src/client/components/core/Css.js +16 -2
  42. package/src/client/components/core/Input.js +3 -1
  43. package/src/client/components/core/Modal.js +125 -159
  44. package/src/client/components/core/Panel.js +436 -31
  45. package/src/client/components/core/PanelForm.js +222 -37
  46. package/src/client/components/core/SearchBox.js +801 -0
  47. package/src/client/components/core/Translate.js +11 -0
  48. package/src/client/services/document/document.service.js +42 -0
  49. package/src/index.js +1 -1
  50. package/src/server/dns.js +12 -6
  51. package/src/server/start.js +14 -6
package/src/cli/lxd.js CHANGED
@@ -74,8 +74,8 @@ class UnderpostLxd {
74
74
  const npmRoot = getNpmRootPath();
75
75
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
76
76
  if (options.reset === true) {
77
- shellExec(`sudo systemctl stop snap.lxd.daemon || true`);
78
- shellExec(`sudo snap remove lxd --purge || true`);
77
+ shellExec(`sudo systemctl stop snap.lxd.daemon`);
78
+ shellExec(`sudo snap remove lxd --purge`);
79
79
  }
80
80
  if (options.install === true) shellExec(`sudo snap install lxd`);
81
81
  if (options.init === true) {
@@ -213,7 +213,7 @@ ipv6.address=none`);
213
213
  for (const port of ports.split(',')) {
214
214
  for (const protocol of protocols) {
215
215
  const deviceName = `${vmName}-${protocol}-port-${port}`;
216
- shellExec(`lxc config device remove ${vmName} ${deviceName} || true`); // Use || true to prevent error if device doesn't exist
216
+ shellExec(`lxc config device remove ${vmName} ${deviceName}`); // Use to prevent error if device doesn't exist
217
217
  shellExec(
218
218
  `lxc config device add ${vmName} ${deviceName} proxy listen=${protocol}:${hostIp}:${port} connect=${protocol}:${vmIp}:${port} nat=true`,
219
219
  );
package/src/cli/run.js CHANGED
@@ -57,6 +57,7 @@ class UnderpostRun {
57
57
  * @property {boolean} force - Whether to force the operation.
58
58
  * @property {boolean} reset - Whether to reset the operation.
59
59
  * @property {boolean} tls - Whether to use TLS.
60
+ * @property {string} cmd - The command to run in the container.
60
61
  * @property {string} tty - The TTY option for the container.
61
62
  * @property {string} stdin - The stdin option for the container.
62
63
  * @property {string} restartPolicy - The restart policy for the container.
@@ -87,6 +88,7 @@ class UnderpostRun {
87
88
  * @property {string} deployId - The deployment ID.
88
89
  * @property {string} instanceId - The instance ID.
89
90
  * @property {string} user - The user to run as.
91
+ * @property {string} pid - The process ID.
90
92
  * @memberof UnderpostRun
91
93
  */
92
94
  static DEFAULT_OPTION = {
@@ -104,6 +106,7 @@ class UnderpostRun {
104
106
  force: false,
105
107
  reset: false,
106
108
  tls: false,
109
+ cmd: '',
107
110
  tty: '',
108
111
  stdin: '',
109
112
  restartPolicy: '',
@@ -134,6 +137,7 @@ class UnderpostRun {
134
137
  deployId: '',
135
138
  instanceId: '',
136
139
  user: '',
140
+ pid: '',
137
141
  };
138
142
  /**
139
143
  * @static
@@ -187,6 +191,7 @@ class UnderpostRun {
187
191
  * @memberof UnderpostRun
188
192
  */
189
193
  kill: (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
194
+ if (options.pid) return shellExec(`sudo kill -9 ${options.pid}`);
190
195
  for (const _path of path.split(',')) {
191
196
  if (_path.split('+')[1]) {
192
197
  let [port, sumPortOffSet] = _path.split('+');
@@ -564,7 +569,7 @@ class UnderpostRun {
564
569
  const currentTraffic = isDeployRunnerContext(path, options)
565
570
  ? UnderpostDeploy.API.getCurrentTraffic(deployId, { namespace: options.namespace })
566
571
  : '';
567
- let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : '';
572
+ let targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'green';
568
573
  if (targetTraffic) versions = targetTraffic;
569
574
 
570
575
  shellExec(
@@ -574,8 +579,13 @@ class UnderpostRun {
574
579
  );
575
580
 
576
581
  if (isDeployRunnerContext(path, options)) {
582
+ const cmdString = options.cmd
583
+ ? ` --cmd ${options.cmd.find((c) => c.match('"')) ? `"${options.cmd}"` : `'${options.cmd}'`}`
584
+ : '';
577
585
  shellExec(
578
- `${baseCommand} deploy --kubeadm --disable-update-proxy ${deployId} ${env} --versions ${versions}${options.namespace ? ` --namespace ${options.namespace}` : ''}`,
586
+ `${baseCommand} deploy --kubeadm${cmdString} --replicas ${
587
+ replicas
588
+ } --disable-update-proxy ${deployId} ${env} --versions ${versions}${options.namespace ? ` --namespace ${options.namespace}` : ''}`,
579
589
  );
580
590
  if (!targetTraffic)
581
591
  targetTraffic = UnderpostDeploy.API.getCurrentTraffic(deployId, { namespace: options.namespace });
@@ -915,7 +925,7 @@ EOF
915
925
  shellExec(`docker exec -i kind-worker bash -c "mkdir -p ${volumeHostPath}"`);
916
926
  shellExec(`docker cp ${volumeHostPath}/engine kind-worker:${volumeHostPath}/engine`);
917
927
  shellExec(
918
- `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${volumeHostPath} || true; chmod -R 755 ${volumeHostPath}"`,
928
+ `docker exec -i kind-worker bash -c "chown -R 1000:1000 ${volumeHostPath}; chmod -R 755 ${volumeHostPath}"`,
919
929
  );
920
930
  } else {
921
931
  shellExec(`kubectl apply -f ${options.underpostRoot}/manifests/pv-pvc-dd.yaml -n ${options.namespace}`);
@@ -1157,6 +1167,7 @@ EOF
1157
1167
  * @memberof UnderpostRun
1158
1168
  */
1159
1169
  cluster: async (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
1170
+ const { underpostRoot } = options;
1160
1171
  const env = options.dev ? 'development' : 'production';
1161
1172
  const baseCommand = options.dev ? 'node bin' : 'underpost';
1162
1173
  const baseClusterCommand = options.dev ? ' --dev' : '';
@@ -1166,12 +1177,16 @@ EOF
1166
1177
  await timer(5000);
1167
1178
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType}`);
1168
1179
  await timer(5000);
1169
- let [runtimeImage, deployList] = path.split(',')
1170
- ? path.split(',')
1171
- : ['express', fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').replaceAll(',', '+')];
1180
+ let [runtimeImage, deployList] =
1181
+ path && path.trim() && path.split(',')
1182
+ ? path.split(',')
1183
+ : [
1184
+ 'express',
1185
+ fs.readFileSync(`${underpostRoot}/engine-private/deploy/dd.router`, 'utf8').replaceAll(',', '+'),
1186
+ ];
1172
1187
  shellExec(
1173
- `${baseCommand} image${baseClusterCommand}${
1174
- runtimeImage ? ` --pull-base --path /home/dd/engine/src/runtime/${runtimeImage}` : ''
1188
+ `${baseCommand} image${baseClusterCommand} --build ${
1189
+ runtimeImage ? ` --pull-base --path ${underpostRoot}/src/runtime/${runtimeImage}` : ''
1175
1190
  } --${clusterType}`,
1176
1191
  );
1177
1192
  if (!deployList) {
@@ -1246,7 +1261,7 @@ EOF
1246
1261
  'disk-clean': async (path, options = UnderpostRun.DEFAULT_OPTION) => {
1247
1262
  const { underpostRoot } = options;
1248
1263
  shellExec(`chmod +x ${underpostRoot}/scripts/disk-clean.sh`);
1249
- shellExec(`./scripts/disk-clean.sh --yes --aggressive`);
1264
+ shellExec(`./scripts/disk-clean.sh`);
1250
1265
  },
1251
1266
 
1252
1267
  /**
@@ -1405,7 +1420,7 @@ EOF
1405
1420
  shellExec(
1406
1421
  `${baseCommand} deploy${options.dev ? '' : ' --kubeadm'}${options.devProxyPortOffset ? ' --disable-deployment-proxy' : ''} --disable-update-deployment ${deployId} ${env} --versions ${versions}`,
1407
1422
  );
1408
- } else logger.error('Mongo Express deployment failed');
1423
+ } else logger.error(`Service pod ${podToMonitor} failed to start in time.`);
1409
1424
  if (options.etcHosts === true) {
1410
1425
  const hostListenResult = UnderpostDeploy.API.etcHostFactory([host]);
1411
1426
  logger.info(hostListenResult.renderHosts);
@@ -1447,6 +1462,32 @@ EOF
1447
1462
  console.log(result);
1448
1463
  },
1449
1464
 
1465
+ /**
1466
+ * @method ps
1467
+ * @description Displays running processes that match a specified path or keyword.
1468
+ * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter processes).
1469
+ * @param {Object} options - The default underpost runner options for customizing workflow
1470
+ * @memberof UnderpostRun
1471
+ */
1472
+ ps: async (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
1473
+ const out = shellExec(`ps aux${path ? `| grep '${path}' | grep -v grep` : ''}`, {
1474
+ stdout: true,
1475
+ silent: true,
1476
+ });
1477
+ console.log(path ? out.replaceAll(path, path.bgYellow.black.bold) : out);
1478
+ },
1479
+
1480
+ /**
1481
+ * @method ptls
1482
+ * @description Set on ~/.bashrc alias: ports <port> Command to list listening ports that match the given keyword.
1483
+ * @param {string} path - The input value, identifier, or path for the operation (used as a keyword to filter listening ports).
1484
+ * @param {Object} options - The default underpost runner options for customizing workflow
1485
+ * @memberof UnderpostRun
1486
+ */
1487
+ ptls: async (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
1488
+ shellExec(`chmod +x ${options.underpostRoot}/scripts/ports-ls.sh`);
1489
+ shellExec(`${options.underpostRoot}/scripts/ports-ls.sh`);
1490
+ },
1450
1491
  /**
1451
1492
  * @method release-cmt
1452
1493
  * @description Commits and pushes a new release for the `engine` repository with a message indicating the new version.
@@ -1462,6 +1503,31 @@ EOF
1462
1503
  shellExec(`underpost push . ${process.env.GITHUB_USERNAME}/engine`, { silent: true });
1463
1504
  },
1464
1505
 
1506
+ /**
1507
+ * @method deploy-test
1508
+ * @description Deploys a test deployment (`dd-test`) in either development or production mode, setting up necessary secrets and starting the deployment.
1509
+ * @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID).
1510
+ * @param {Object} options - The default underpost runner options for customizing workflow
1511
+ * @memberof UnderpostRun
1512
+ */
1513
+ 'deploy-test': async (path, options = UnderpostRun.DEFAULT_OPTION) => {
1514
+ // Note: use recomendation empty deploy cluster: node bin --dev cluster
1515
+ const env = options.dev ? 'development' : 'production';
1516
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
1517
+ const baseClusterCommand = options.dev ? ' --dev' : '';
1518
+ const inputs = path ? path.split(',') : [];
1519
+ const deployId = inputs[0] ? inputs[0] : 'dd-test';
1520
+ const cmd = options.cmd
1521
+ ? options.cmd
1522
+ : [
1523
+ `npm install -g npm@11.2.0`,
1524
+ `npm install -g underpost`,
1525
+ `${baseCommand} secret underpost --create-from-file /etc/config/.env.${env}`,
1526
+ `${baseCommand} start --build --run ${deployId} ${env} --underpost-quickly-install`,
1527
+ ];
1528
+ shellExec(`node bin run sync${baseClusterCommand} --cron-jobs none dd-test --cmd "${cmd}"`);
1529
+ },
1530
+
1465
1531
  /**
1466
1532
  * @method sync-replica
1467
1533
  * @description Syncs a replica for the dd.router
@@ -1597,7 +1663,7 @@ ${hostNetwork ? ` hostNetwork: ${hostNetwork}` : ''}
1597
1663
  imagePullPolicy: ${imagePullPolicy}
1598
1664
  tty: ${tty}
1599
1665
  stdin: ${stdin}
1600
- command: ${JSON.stringify(options.command ? options.command : ['/bin/bash', '-c'])}
1666
+ command: ${JSON.stringify(options.cmd ? options.cmd : ['/bin/bash', '-c'])}
1601
1667
  ${
1602
1668
  args.length > 0
1603
1669
  ? ` args:
@@ -1649,7 +1715,7 @@ EOF`;
1649
1715
  try {
1650
1716
  const npmRoot = getNpmRootPath();
1651
1717
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
1652
- if (options.command) options.command = options.command.split(',');
1718
+ if (options.cmd) options.cmd = options.cmd.split(',');
1653
1719
  if (options.args) options.args = options.args.split(',');
1654
1720
  if (!options.underpostRoot) options.underpostRoot = underpostRoot;
1655
1721
  if (!options.namespace) options.namespace = 'default';
package/src/cli/ssh.js CHANGED
@@ -280,7 +280,7 @@ EOF`);
280
280
  options.password = confNode.users[options.user].password;
281
281
  logger.info(`Using saved password for user ${options.user}`);
282
282
  }
283
- options.port = confNode.users[options.user].port || options.port;
283
+ options.port = options.port || confNode.users[options.user].port || 22;
284
284
  }
285
285
  }
286
286
 
@@ -865,10 +865,17 @@ const subThemeManager = {
865
865
  return html`<style>
866
866
  button:hover,
867
867
  .a-btn:hover,
868
- .main-btn-menu-active {
868
+ .main-btn-menu-active,
869
+ .top-bar-search-box-container:hover {
869
870
  color: ${this.lightColor};
870
871
  background-color: ${lightenHex(this.lightColor, 0.8)};
871
872
  }
873
+ .top-bar-search-box-container i {
874
+ color: #000;
875
+ }
876
+ .top-bar-search-box-container:hover i {
877
+ color: ${this.lightColor};
878
+ }
872
879
  .main-sub-btn-active {
873
880
  color: ${this.lightColor};
874
881
  background-color: rgba(0, 0, 0, 0.3);
@@ -887,10 +894,17 @@ const subThemeManager = {
887
894
  return html`<style>
888
895
  button:hover,
889
896
  .a-btn:hover,
890
- .main-btn-menu-active {
897
+ .main-btn-menu-active,
898
+ .top-bar-search-box-container:hover {
891
899
  color: ${lightenHex(this.darkColor, 0.8)};
892
900
  background-color: ${darkenHex(this.darkColor, 0.75)};
893
901
  }
902
+ .top-bar-search-box-container i {
903
+ color: #fff;
904
+ }
905
+ .top-bar-search-box-container:hover i {
906
+ color: ${lightenHex(this.darkColor, 0.8)};
907
+ }
894
908
  .main-sub-btn-active {
895
909
  color: ${lightenHex(this.darkColor, 0.8)};
896
910
  background-color: rgba(255, 255, 255, 0.3);
@@ -162,7 +162,9 @@ const Input = {
162
162
  break;
163
163
  }
164
164
  case 'md':
165
- RichText.Tokens[inputData.id].easyMDE.value('');
165
+ setTimeout(() => {
166
+ RichText.Tokens[inputData.id].easyMDE.value('');
167
+ });
166
168
  continue;
167
169
  break;
168
170
  case 'checkbox':
@@ -34,6 +34,7 @@ import { Badge } from './Badge.js';
34
34
  import { Worker } from './Worker.js';
35
35
  import { Scroll } from './Scroll.js';
36
36
  import { windowGetH, windowGetW } from './windowGetDimensions.js';
37
+ import { SearchBox } from './SearchBox.js';
37
38
 
38
39
  const logger = loggerFactory(import.meta, { trace: true });
39
40
 
@@ -549,6 +550,9 @@ const Modal = {
549
550
  const inputInfoNode = s(`.input-info-${inputSearchBoxId}`).cloneNode(true);
550
551
  s(`.input-info-${inputSearchBoxId}`).remove();
551
552
  {
553
+ // Inject SearchBox base styles
554
+ SearchBox.injectStyles();
555
+
552
556
  const id = 'search-box-history';
553
557
  const searchBoxHistoryId = id;
554
558
  const formDataInfoNode = [
@@ -591,6 +595,7 @@ const Modal = {
591
595
 
592
596
  const renderSearchResult = async (results) => {
593
597
  htmls(`.html-${searchBoxHistoryId}`, '');
598
+
594
599
  if (results.length === 0) {
595
600
  append(
596
601
  `.html-${searchBoxHistoryId}`,
@@ -609,108 +614,61 @@ const Modal = {
609
614
  }),
610
615
  }),
611
616
  );
617
+ return;
612
618
  }
613
- let indexResult = -1;
614
- for (const result of results) {
615
- indexResult++;
616
- const indexRender = indexResult;
617
- append(
618
- `.html-${searchBoxHistoryId}`,
619
- await BtnIcon.Render({
620
- label: `${
621
- result.fontAwesomeIcon
622
- ? html`<i class="${result.fontAwesomeIcon.classList.toString()}"></i> `
623
- : result.imgElement
624
- ? html`<img
625
- class="inl"
626
- src="${result.imgElement.src}"
627
- style="${renderCssAttr({ style: { width: '25px', height: '25px' } })}"
628
- />`
629
- : ''
630
- } ${Translate.Render(result.routerId)}`,
631
- class: `wfa search-result-btn-${result.routerId} ${
632
- indexResult === currentKeyBoardSearchBoxIndex ? 'main-btn-menu-active' : ''
633
- } search-result-btn-${indexResult}`,
634
- style: renderCssAttr({
635
- style: { padding: '3px', margin: '2px', 'text-align': 'left' },
636
- }),
637
- }),
638
- );
639
- s(`.search-result-btn-${result.routerId}`).onclick = () => {
640
- if (!s(`.html-${searchBoxHistoryId}`) || !s(`.html-${searchBoxHistoryId}`).hasChildNodes()) return;
641
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.remove(
642
- `main-btn-menu-active`,
643
- );
644
- currentKeyBoardSearchBoxIndex = indexRender;
645
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.add(
646
- `main-btn-menu-active`,
647
- );
648
- setSearchValue(`.search-result-btn-${result.routerId}`);
649
- };
650
- }
619
+
620
+ // Use SearchBox component for rendering results
621
+ const searchContext = {
622
+ RouterInstance: Worker.RouterInstance,
623
+ options: options,
624
+ onResultClick: () => {
625
+ // Dismiss search box on result click
626
+ if (s(`.${searchBoxHistoryId}`)) {
627
+ Modal.removeModal(searchBoxHistoryId);
628
+ }
629
+ },
630
+ };
631
+
632
+ SearchBox.renderResults(results, `html-${searchBoxHistoryId}`, searchContext);
651
633
  };
652
634
 
653
- const getResultSearchBox = (validatorData) => {
654
- if (!s(`.html-${searchBoxHistoryId}`) || !s(`.html-${searchBoxHistoryId}`).hasChildNodes()) return;
635
+ const getResultSearchBox = async (validatorData) => {
636
+ if (!s(`.html-${searchBoxHistoryId}`)) return;
655
637
  const { model, id } = validatorData;
638
+
656
639
  switch (model) {
657
640
  case 'search-box':
658
641
  {
659
- if (
660
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex] &&
661
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList
662
- )
663
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.remove(
664
- `main-btn-menu-active`,
665
- );
666
642
  currentKeyBoardSearchBoxIndex = 0;
667
- if (
668
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex] &&
669
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList
670
- )
671
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.add(
672
- `main-btn-menu-active`,
673
- );
674
643
  results = [];
675
- const routerInstance = Worker.RouterInstance.Routes();
676
- for (const _routerId of Object.keys(routerInstance)) {
677
- const routerId = _routerId.slice(1);
678
- if (routerId) {
679
- if (
680
- s(`.main-btn-${routerId}`) &&
681
- (routerId.toLowerCase().match(s(`.${id}`).value.toLowerCase()) ||
682
- (Translate.Data[routerId] &&
683
- Object.keys(Translate.Data[routerId]).filter((keyLang) =>
684
- Translate.Data[routerId][keyLang]
685
- .toLowerCase()
686
- .match(s(`.${id}`).value.toLowerCase()),
687
- ).length > 0))
688
- ) {
689
- const fontAwesomeIcon = getAllChildNodes(s(`.main-btn-${routerId}`)).find((e) => {
690
- return (
691
- e.classList &&
692
- Array.from(e.classList).find((e) => e.match('fa-') && !e.match('fa-grip-vertical'))
693
- );
694
- });
695
- const imgElement = getAllChildNodes(s(`.main-btn-${routerId}`)).find((e) => {
696
- return (
697
- e.classList &&
698
- Array.from(e.classList).find((e) =>
699
- options.searchCustomImgClass
700
- ? e.match(options.searchCustomImgClass)
701
- : e.match('img-btn-square-menu'),
702
- )
703
- );
704
- });
705
- if (imgElement || fontAwesomeIcon) {
706
- results.push({
707
- routerId,
708
- fontAwesomeIcon: fontAwesomeIcon,
709
- imgElement,
710
- });
711
- }
644
+
645
+ const query = s(`.${id}`) ? s(`.${id}`).value : '';
646
+ const trimmedQuery = query.trim();
647
+
648
+ // Use SearchBox component for extensible search
649
+ const searchContext = {
650
+ RouterInstance: Worker.RouterInstance,
651
+ options: options,
652
+ minQueryLength: options?.minSearchQueryLength || 1, // Allow single character search by default
653
+ onResultClick: () => {
654
+ // Dismiss search box on result click
655
+ if (s(`.${searchBoxHistoryId}`)) {
656
+ Modal.removeModal(searchBoxHistoryId);
712
657
  }
713
- }
658
+ },
659
+ };
660
+
661
+ // Check minimum query length (default: 1 character)
662
+ const minLength = searchContext.minQueryLength;
663
+ if (trimmedQuery.length >= minLength) {
664
+ results = await SearchBox.search(trimmedQuery, searchContext);
665
+ renderSearchResult(results);
666
+ } else if (trimmedQuery.length === 0) {
667
+ // Show history when query is empty
668
+ renderSearchResult(historySearchBox);
669
+ } else {
670
+ // Query is too short - show nothing or a hint
671
+ renderSearchResult([]);
714
672
  }
715
673
  }
716
674
  break;
@@ -718,8 +676,6 @@ const Modal = {
718
676
  default:
719
677
  break;
720
678
  }
721
- if (s(`.${inputSearchBoxId}`).value.trim()) renderSearchResult(results);
722
- else renderSearchResult(historySearchBox);
723
679
  };
724
680
 
725
681
  const searchBoxCallBack = async (validatorData) => {
@@ -745,25 +701,19 @@ const Modal = {
745
701
  const getDefaultSearchBoxSelector = () => `.search-result-btn-${currentKeyBoardSearchBoxIndex}`;
746
702
 
747
703
  const updateSearchBoxValue = (selector) => {
748
- if (!selector) selector = getDefaultSearchBoxSelector();
749
- // check exist childNodes
750
- if (!s(selector) || !s(selector).hasChildNodes()) return;
751
-
752
- if (s(selector).childNodes) {
753
- if (
754
- s(selector).childNodes[s(selector).childNodes.length - 1] &&
755
- s(selector).childNodes[s(selector).childNodes.length - 1].data &&
756
- s(selector).childNodes[s(selector).childNodes.length - 1].data.trim()
757
- ) {
758
- s(`.${inputSearchBoxId}`).value =
759
- s(selector).childNodes[s(selector).childNodes.length - 1].data.trim();
760
- } else if (
761
- s(selector).childNodes[s(selector).childNodes.length - 2] &&
762
- s(selector).childNodes[s(selector).childNodes.length - 2].outerText &&
763
- s(selector).childNodes[s(selector).childNodes.length - 2].outerText.trim()
764
- ) {
765
- s(`.${inputSearchBoxId}`).value =
766
- s(selector).childNodes[s(selector).childNodes.length - 2].outerText.trim();
704
+ if (!selector) {
705
+ // Get the currently active search result item
706
+ const activeItem = s(`.html-${searchBoxHistoryId} .search-result-item.active-search-result`);
707
+ if (activeItem) {
708
+ const titleEl = activeItem.querySelector('.search-result-title');
709
+ if (titleEl && titleEl.textContent) {
710
+ s(`.${inputSearchBoxId}`).value = titleEl.textContent.trim();
711
+ }
712
+ }
713
+ } else if (s(selector)) {
714
+ const titleEl = s(selector).querySelector('.search-result-title');
715
+ if (titleEl && titleEl.textContent) {
716
+ s(`.${inputSearchBoxId}`).value = titleEl.textContent.trim();
767
717
  }
768
718
  }
769
719
  checkHistoryBoxTitleStatus();
@@ -771,17 +721,29 @@ const Modal = {
771
721
  };
772
722
 
773
723
  const setSearchValue = (selector) => {
774
- if (!selector) selector = getDefaultSearchBoxSelector();
724
+ // Get all search result items
725
+ const allItems = sa(`.html-${searchBoxHistoryId} .search-result-item`);
726
+ if (!allItems || allItems.length === 0) return;
775
727
 
776
- // check exist childNodes
777
- if (!s(selector) || !s(selector).hasChildNodes()) return;
728
+ const activeItem = allItems[currentKeyBoardSearchBoxIndex];
729
+ if (!activeItem) return;
778
730
 
779
- historySearchBox = historySearchBox.filter(
780
- (h) => h.routerId !== results[currentKeyBoardSearchBoxIndex].routerId,
781
- );
782
- historySearchBox.unshift(results[currentKeyBoardSearchBoxIndex]);
783
- updateSearchBoxValue(selector);
784
- s(`.main-btn-${results[currentKeyBoardSearchBoxIndex].routerId}`).click();
731
+ const resultId = activeItem.getAttribute('data-result-id');
732
+ const resultType = activeItem.getAttribute('data-result-type');
733
+
734
+ if (resultType === 'route' && results[currentKeyBoardSearchBoxIndex]) {
735
+ historySearchBox = historySearchBox.filter(
736
+ (h) => h.routerId !== results[currentKeyBoardSearchBoxIndex].routerId,
737
+ );
738
+ historySearchBox.unshift(results[currentKeyBoardSearchBoxIndex]);
739
+ updateSearchBoxValue();
740
+ if (s(`.main-btn-${resultId}`)) {
741
+ s(`.main-btn-${resultId}`).click();
742
+ }
743
+ } else {
744
+ // Trigger click on custom result
745
+ activeItem.click();
746
+ }
785
747
  Modal.removeModal(searchBoxHistoryId);
786
748
  };
787
749
  let boxHistoryDelayRender = 0;
@@ -879,27 +841,29 @@ const Modal = {
879
841
  keys: ['ArrowUp'],
880
842
  eventCallBack: () => {
881
843
  if (s(`.${id}`)) {
882
- if (
883
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex] &&
884
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex - 1]
885
- ) {
886
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.remove(
887
- `main-btn-menu-active`,
888
- );
844
+ const allItems = sa(`.html-${searchBoxHistoryId} .search-result-item`);
845
+ if (!allItems || allItems.length === 0) return;
846
+
847
+ // Remove active class from current
848
+ if (allItems[currentKeyBoardSearchBoxIndex]) {
849
+ allItems[currentKeyBoardSearchBoxIndex].classList.remove('active-search-result');
850
+ }
851
+
852
+ // Navigate up
853
+ if (currentKeyBoardSearchBoxIndex > 0) {
889
854
  currentKeyBoardSearchBoxIndex--;
890
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.add(
891
- `main-btn-menu-active`,
892
- );
893
855
  } else {
894
- if (s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex])
895
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.remove(
896
- `main-btn-menu-active`,
897
- );
898
- currentKeyBoardSearchBoxIndex = s(`.html-${searchBoxHistoryId}`).childNodes.length - 1;
899
- if (s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex])
900
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.add(
901
- `main-btn-menu-active`,
902
- );
856
+ currentKeyBoardSearchBoxIndex = allItems.length - 1;
857
+ }
858
+
859
+ // Add active class to new and ensure visibility
860
+ if (allItems[currentKeyBoardSearchBoxIndex]) {
861
+ allItems[currentKeyBoardSearchBoxIndex].classList.add('active-search-result');
862
+ // Use optimized scroll method to ensure item is always visible
863
+ const container = s(`.html-${searchBoxHistoryId}`);
864
+ if (container) {
865
+ SearchBox.scrollIntoViewIfNeeded(allItems[currentKeyBoardSearchBoxIndex], container);
866
+ }
903
867
  }
904
868
  updateSearchBoxValue();
905
869
  }
@@ -912,27 +876,29 @@ const Modal = {
912
876
  keys: ['ArrowDown'],
913
877
  eventCallBack: () => {
914
878
  if (s(`.${id}`)) {
915
- if (
916
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex] &&
917
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex + 1]
918
- ) {
919
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.remove(
920
- `main-btn-menu-active`,
921
- );
879
+ const allItems = sa(`.html-${searchBoxHistoryId} .search-result-item`);
880
+ if (!allItems || allItems.length === 0) return;
881
+
882
+ // Remove active class from current
883
+ if (allItems[currentKeyBoardSearchBoxIndex]) {
884
+ allItems[currentKeyBoardSearchBoxIndex].classList.remove('active-search-result');
885
+ }
886
+
887
+ // Navigate down
888
+ if (currentKeyBoardSearchBoxIndex < allItems.length - 1) {
922
889
  currentKeyBoardSearchBoxIndex++;
923
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.add(
924
- `main-btn-menu-active`,
925
- );
926
890
  } else {
927
- if (s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex])
928
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.remove(
929
- `main-btn-menu-active`,
930
- );
931
891
  currentKeyBoardSearchBoxIndex = 0;
932
- if (s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex])
933
- s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.add(
934
- `main-btn-menu-active`,
935
- );
892
+ }
893
+
894
+ // Add active class to new and ensure visibility
895
+ if (allItems[currentKeyBoardSearchBoxIndex]) {
896
+ allItems[currentKeyBoardSearchBoxIndex].classList.add('active-search-result');
897
+ // Use optimized scroll method to ensure item is always visible
898
+ const container = s(`.html-${searchBoxHistoryId}`);
899
+ if (container) {
900
+ SearchBox.scrollIntoViewIfNeeded(allItems[currentKeyBoardSearchBoxIndex], container);
901
+ }
936
902
  }
937
903
  updateSearchBoxValue();
938
904
  }