kop-cli 0.2.0b1__tar.gz → 0.2.0b2__tar.gz

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 (155) hide show
  1. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/PKG-INFO +1 -1
  2. kop_cli-0.2.0b2/scripts/set_release_version.py +52 -0
  3. kop_cli-0.2.0b2/src/kop/__init__.py +2 -0
  4. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/fields.py +1 -1
  5. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/ResourceView.py +26 -13
  6. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Panel.py +28 -2
  7. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_resource_view.py +112 -7
  8. kop_cli-0.2.0b1/src/kop/__init__.py +0 -2
  9. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/.gitignore +0 -0
  10. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/.python-version +0 -0
  11. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/CODE_OF_CONDUCT.md +0 -0
  12. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/LICENSE +0 -0
  13. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/NOTICE +0 -0
  14. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/README.md +0 -0
  15. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/__init__.py +0 -0
  16. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/faq.md +0 -0
  17. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/getting_started.md +0 -0
  18. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/ClusterRoleBindings.md +0 -0
  19. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/ClusterRoles.md +0 -0
  20. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Clusters.md +0 -0
  21. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/ConfigMaps.md +0 -0
  22. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/CronJobs.md +0 -0
  23. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/DaemonSets.md +0 -0
  24. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Deployments.md +0 -0
  25. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/EndpointSlices.md +0 -0
  26. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Endpoints.md +0 -0
  27. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/IngressClasses.md +0 -0
  28. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Ingresses.md +0 -0
  29. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Jobs.md +0 -0
  30. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Namespaces.md +0 -0
  31. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/NetworkPolicies.md +0 -0
  32. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Nodes.md +0 -0
  33. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/PersistentVolumeClaims.md +0 -0
  34. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/PersistentVolumes.md +0 -0
  35. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Pods.md +0 -0
  36. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/RoleBindings.md +0 -0
  37. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Roles.md +0 -0
  38. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Secrets.md +0 -0
  39. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/ServiceAccounts.md +0 -0
  40. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Services.md +0 -0
  41. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/StatefulSets.md +0 -0
  42. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/StorageClasses.md +0 -0
  43. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/index.md +0 -0
  44. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/zh/pods-zh.md +0 -0
  45. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide.md +0 -0
  46. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/create_resource.png +0 -0
  47. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/edit_resource.png +0 -0
  48. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/forward_port.png +0 -0
  49. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/pod_logs.png +0 -0
  50. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/resource_detail.png +0 -0
  51. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/scale_resource.png +0 -0
  52. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/search_kinds.png +0 -0
  53. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/select_namespace.png +0 -0
  54. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/sample.png +0 -0
  55. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/action_workspace.png +0 -0
  56. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/add_new_cluster.png +0 -0
  57. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/cluster_area.png +0 -0
  58. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/detail_view.png +0 -0
  59. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/keys.png +0 -0
  60. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/resource_view.png +0 -0
  61. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/startup.png +0 -0
  62. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/startup_without_cluster.png +0 -0
  63. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/themes.png +0 -0
  64. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/index.md +0 -0
  65. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/readme_ch.md +0 -0
  66. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/tutorial.md +0 -0
  67. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/mkdocs.yml +0 -0
  68. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/pyproject.toml +0 -0
  69. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/event.py +0 -0
  70. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/forward.py +0 -0
  71. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/kube_client.py +0 -0
  72. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/mock_resource_view_deployment.py +0 -0
  73. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/mock_resource_view_pod.py +0 -0
  74. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/multiple_select.py +0 -0
  75. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/pixels.py +0 -0
  76. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/pod_logs.py +0 -0
  77. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/rich_detail.py +0 -0
  78. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/view_pod_logs.py +0 -0
  79. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/app/__init__.py +0 -0
  80. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/app/main.py +0 -0
  81. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/controllers/handler.py +0 -0
  82. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/factory.py +0 -0
  83. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/models.py +0 -0
  84. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/attach.py +0 -0
  85. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/client.py +0 -0
  86. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/config.py +0 -0
  87. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/events.py +0 -0
  88. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/exec.py +0 -0
  89. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/forward.py +0 -0
  90. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/logs.py +0 -0
  91. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/utils.py +0 -0
  92. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/registry.py +0 -0
  93. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/details.py +0 -0
  94. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/formatter.py +0 -0
  95. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/forms.py +0 -0
  96. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/table.py +0 -0
  97. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/clusterrolebindings.yaml +0 -0
  98. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/clusterroles.yaml +0 -0
  99. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/configmaps.yaml +0 -0
  100. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/cronjobs.yaml +0 -0
  101. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/daemonsets.yaml +0 -0
  102. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/deployments.yaml +0 -0
  103. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/endpointslices.yaml +0 -0
  104. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/ingressclasses.yaml +0 -0
  105. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/ingresses.yaml +0 -0
  106. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/jobs.yaml +0 -0
  107. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/namespaces.yaml +0 -0
  108. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/networkpolicies.yaml +0 -0
  109. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/node-shell-pod.yaml +0 -0
  110. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/persistentvolumeclaims.yaml +0 -0
  111. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/persistentvolumes.yaml +0 -0
  112. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/pods.yaml +0 -0
  113. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/rolebindings.yaml +0 -0
  114. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/roles.yaml +0 -0
  115. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/secrets.yaml +0 -0
  116. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/serviceaccounts.yaml +0 -0
  117. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/services.yaml +0 -0
  118. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/statefulsets.yaml +0 -0
  119. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/storageclasses.yaml +0 -0
  120. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/validations.py +0 -0
  121. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/ActionWorkspace.py +0 -0
  122. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/EditView.py +0 -0
  123. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/PodAttach.py +0 -0
  124. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/PodLog.py +0 -0
  125. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/PodTerminal.py +0 -0
  126. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/StartupView.py +0 -0
  127. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Actions.py +0 -0
  128. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Attach.py +0 -0
  129. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Columns.py +0 -0
  130. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Detail.py +0 -0
  131. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Directory.py +0 -0
  132. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Dynamic.py +0 -0
  133. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Edit.py +0 -0
  134. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Events.py +0 -0
  135. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Expandable.py +0 -0
  136. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Focusable.py +0 -0
  137. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Forward.py +0 -0
  138. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Log.py +0 -0
  139. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Modals.py +0 -0
  140. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/MultipleSelect.py +0 -0
  141. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Pty.py +0 -0
  142. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/RichDetail.py +0 -0
  143. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Rules.py +0 -0
  144. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/SideMenu.py +0 -0
  145. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/__init__.py +0 -0
  146. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_action_workspace.py +0 -0
  147. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_detail_modal_renderer.py +0 -0
  148. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_edit_view.py +0 -0
  149. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_event_service.py +0 -0
  150. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_log_controller.py +0 -0
  151. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_pagination_integration.py +0 -0
  152. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_pod_logs.py +0 -0
  153. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_startup_view.py +0 -0
  154. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_table_renderer.py +0 -0
  155. {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kop-cli
3
- Version: 0.2.0b1
3
+ Version: 0.2.0b2
4
4
  Summary: Terminal-based kubernetes operation platform
5
5
  Author-email: vegaoqiang <vegaoqiang@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """Set the source package version using PEP 440 normalization."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import re
8
+ from pathlib import Path
9
+
10
+ try:
11
+ from packaging.version import Version
12
+ except ModuleNotFoundError:
13
+ from pip._vendor.packaging.version import Version
14
+
15
+
16
+ VERSION_PATTERN = re.compile(r'^__version__\s*=\s*["\']([^"\']+)["\']\s*$')
17
+
18
+
19
+ def read_version(path: Path) -> str:
20
+ for line in path.read_text(encoding="utf-8").splitlines():
21
+ match = VERSION_PATTERN.match(line)
22
+ if match:
23
+ return match.group(1)
24
+ raise SystemExit(f"Unable to find __version__ in {path}")
25
+
26
+
27
+ def write_version(path: Path, version: str) -> None:
28
+ lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
29
+ for index, line in enumerate(lines):
30
+ if VERSION_PATTERN.match(line.strip()):
31
+ newline = "\n" if line.endswith("\n") else ""
32
+ lines[index] = f'__version__ = "{version}"{newline}'
33
+ path.write_text("".join(lines), encoding="utf-8")
34
+ return
35
+ raise SystemExit(f"Unable to find __version__ in {path}")
36
+
37
+
38
+ def main() -> None:
39
+ parser = argparse.ArgumentParser()
40
+ parser.add_argument("version", nargs="?")
41
+ parser.add_argument("--path", default="src/kop/__init__.py")
42
+ args = parser.parse_args()
43
+
44
+ version_path = Path(args.path)
45
+ if args.version:
46
+ write_version(version_path, str(Version(args.version)))
47
+
48
+ print(read_version(version_path))
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,2 @@
1
+ __version__ = "0.2.0b2"
2
+ __git_commit__ = ""
@@ -38,7 +38,7 @@ def container_status_renderer(value):
38
38
  if ephemeral_container_statuses is None:
39
39
  ephemeral_container_statuses = []
40
40
 
41
- all_container_status = value.container_statuses + init_container_statuses + ephemeral_container_statuses
41
+ all_container_status = value.container_statuses or [] + init_container_statuses + ephemeral_container_statuses
42
42
  status_texts = []
43
43
  for cs in all_container_status:
44
44
  if cs.ready and cs.started:
@@ -94,7 +94,7 @@ class ResourceView(Screen):
94
94
  self.resource_type: Optional[str] = None
95
95
  self.resource_kind_name: Optional[str] = None
96
96
  self.page_index: int = 0
97
- self.resource_pages: dict[str, list[tuple[object, list, Optional[str]]]] = {}
97
+ self.resource_pages: dict[str, list[tuple[object, list, Optional[str], int]]] = {}
98
98
 
99
99
  def compose(self) -> ComposeResult:
100
100
  yield Header()
@@ -179,10 +179,10 @@ class ResourceView(Screen):
179
179
  namespace: Optional[str],
180
180
  keyword: Optional[str],
181
181
  continue_token: Optional[str] = None,
182
- ) -> Tuple[Optional[BaseFactory], Optional[object], list]:
182
+ ) -> Tuple[Optional[BaseFactory], Optional[object], list, int]:
183
183
  factory_cls = ResourceRegistry.get_factory(resource_type)
184
184
  if not factory_cls:
185
- return None, None, []
185
+ return None, None, [], 0
186
186
  factory = factory_cls(self.endpoint)
187
187
  try:
188
188
  data = factory.fetch(
@@ -192,11 +192,10 @@ class ResourceView(Screen):
192
192
  )
193
193
  except TypeError:
194
194
  data = factory.fetch(namespace=namespace)
195
- if keyword:
196
- data = factory.filter(data, keyword)
197
- cleaned = factory.clean(data)
195
+ filtered = factory.filter(data, keyword) if keyword else data
196
+ cleaned = factory.clean(filtered)
198
197
  cleaned.sort(key=lambda vm: vm.name)
199
- return factory, data, cleaned
198
+ return factory, data, cleaned, len(filtered.items)
200
199
 
201
200
  def _fetch_resource_with_timeout(
202
201
  self,
@@ -205,7 +204,7 @@ class ResourceView(Screen):
205
204
  keyword: Optional[str],
206
205
  timeout: float,
207
206
  continue_token: Optional[str] = None,
208
- ) -> Tuple[Optional[BaseFactory], Optional[object], list]:
207
+ ) -> Tuple[Optional[BaseFactory], Optional[object], list, int]:
209
208
  """Run kubernetes fetch in a daemon thread so app shutdown isn't blocked by stuck API calls."""
210
209
  result: Queue[Tuple[str, object]] = Queue(maxsize=1)
211
210
 
@@ -264,7 +263,9 @@ class ResourceView(Screen):
264
263
  page_index: int,
265
264
  factory: Optional[BaseFactory],
266
265
  data: Optional[object],
266
+ keyword: Optional[str],
267
267
  cleaned: list,
268
+ resource_count: int,
268
269
  ) -> None:
269
270
  if request_id != self._resource_request_id:
270
271
  return
@@ -274,10 +275,15 @@ class ResourceView(Screen):
274
275
 
275
276
  self.FACTORY_CACHE = factory
276
277
  self.data = data
278
+ if keyword != self.keyword:
279
+ filtered = factory.filter(data, self.keyword) if self.keyword else data
280
+ cleaned = factory.clean(filtered)
281
+ cleaned.sort(key=lambda vm: vm.name)
282
+ resource_count = len(filtered.items)
277
283
  next_token = getattr(getattr(data, "metadata", None), "_continue", None)
278
284
  cache_key = self._resource_cache_key(resource_type, namespace)
279
285
  pages = self.resource_pages.setdefault(cache_key, [])
280
- page_entry = (data, cleaned, next_token)
286
+ page_entry = (data, cleaned, next_token, resource_count)
281
287
  if page_index < len(pages):
282
288
  pages[page_index] = page_entry
283
289
  del pages[page_index + 1 :]
@@ -296,7 +302,7 @@ class ResourceView(Screen):
296
302
  self.table.raw_data = data.items
297
303
  self.table.data = cleaned
298
304
 
299
- self.panel.resource_count = len(data.items)
305
+ self.panel.resource_count = resource_count
300
306
 
301
307
  def _handle_resource_error(self, request_id: int, exc: Exception) -> None:
302
308
  if request_id != self._resource_request_id:
@@ -316,7 +322,7 @@ class ResourceView(Screen):
316
322
  ) -> None:
317
323
  worker = get_current_worker()
318
324
  try:
319
- factory, data, cleaned = self._fetch_resource_with_timeout(
325
+ factory, data, cleaned, resource_count = self._fetch_resource_with_timeout(
320
326
  resource_type,
321
327
  namespace,
322
328
  keyword,
@@ -337,7 +343,9 @@ class ResourceView(Screen):
337
343
  page_index,
338
344
  factory,
339
345
  data,
346
+ keyword,
340
347
  cleaned,
348
+ resource_count,
341
349
  )
342
350
 
343
351
  def _load_resource(
@@ -367,14 +375,19 @@ class ResourceView(Screen):
367
375
  pages = self.resource_pages.get(cache_key, [])
368
376
  if page_index < 0 or page_index >= len(pages):
369
377
  return False
370
- data, cleaned, _ = pages[page_index]
378
+ if not self.FACTORY_CACHE:
379
+ return False
380
+ data, _cleaned, _continue_token, _resource_count = pages[page_index]
371
381
  self.data = data
372
382
  self.page_index = page_index
383
+ filtered = self.FACTORY_CACHE.filter(data, self.keyword) if self.keyword else data
384
+ cleaned = self.FACTORY_CACHE.clean(filtered)
385
+ cleaned.sort(key=lambda vm: vm.name)
373
386
  if self.table:
374
387
  self.table.raw_data = data.items
375
388
  self.table.data = cleaned
376
389
  if self.panel:
377
- self.panel.resource_count = len(data.items)
390
+ self.panel.resource_count = len(filtered.items)
378
391
  self.refresh_bindings()
379
392
  return True
380
393
 
@@ -2,7 +2,8 @@ from textual.message import Message
2
2
  from textual.events import Mount
3
3
  from textual.reactive import Reactive
4
4
  from textual.app import ComposeResult
5
- from textual.widgets import Static, Input, Label, Select
5
+ from textual.css.query import NoMatches
6
+ from textual.widgets import Static, Input, Label, Select, ListView
6
7
  from textual.containers import Grid, Horizontal
7
8
  from textual.timer import Timer
8
9
  from textual.binding import Binding
@@ -66,6 +67,7 @@ class ResourcePanel(Static):
66
67
  debounce_time: float = 0.3
67
68
 
68
69
  BINDINGS = [
70
+ Binding(key="tab", action="focus_table", show=False),
69
71
  Binding(key="escape", action="clear", show=False),
70
72
  ]
71
73
 
@@ -128,6 +130,30 @@ class ResourcePanel(Static):
128
130
  def _on_mount(self, event: Mount) -> None:
129
131
  self.post_message(self.RequireNamespace().set_sender(self))
130
132
 
133
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> Optional[bool]:
134
+ if action != "focus_table":
135
+ return True
136
+ focused = self.app.focused
137
+ if not isinstance(focused, (Input, Select)):
138
+ return False
139
+
140
+ parent = focused.parent
141
+ while parent is not None:
142
+ if parent is self:
143
+ break
144
+ parent = parent.parent
145
+ else:
146
+ return False
147
+
148
+ try:
149
+ self.screen.query_one("#list_view", ListView)
150
+ except NoMatches:
151
+ return False
152
+ return True
153
+
154
+ def action_focus_table(self) -> None:
155
+ self.screen.query_one("#list_view", ListView).focus()
156
+
131
157
  def action_clear(self) -> None:
132
158
  """
133
159
  clear search input
@@ -147,4 +173,4 @@ class ResourcePanel(Static):
147
173
  def __init__(self, query: str) -> None:
148
174
  super().__init__()
149
175
  self.query = query
150
-
176
+
@@ -6,8 +6,11 @@ from unittest.mock import MagicMock
6
6
 
7
7
  from textual.app import App
8
8
  from textual.screen import Screen
9
+ from textual.widgets import ListView
9
10
 
11
+ from kop.models import ColumnModel
10
12
  from kop.registry import ResourceRegistry
13
+ from kop.renderers.table import TableRenderer
11
14
  from kop.views.ResourceView import ResourceView
12
15
  from kop.widgets.Panel import ResourcePanel
13
16
  from kop.controllers.handler import BaseActionHandlerMixin
@@ -36,6 +39,46 @@ class _FakeActiveTimer:
36
39
  self.reset_called = True
37
40
 
38
41
 
42
+ class _Row(dict):
43
+ def __getattr__(self, item):
44
+ try:
45
+ return self[item]
46
+ except KeyError as exc:
47
+ raise AttributeError(item) from exc
48
+
49
+
50
+ def test_resource_panel_tab_focuses_table_list_view() -> None:
51
+ app = ResourceHarnessApp()
52
+
53
+ async def _run() -> None:
54
+ async with app.run_test(size=(120, 40)) as pilot:
55
+ await pilot.pause()
56
+ view = app.view
57
+ assert view is not None
58
+
59
+ table = TableRenderer(
60
+ columns=[ColumnModel(title="Name", width=1, field="name")],
61
+ data=[_Row(name="pod-a")],
62
+ raw_data=[SimpleNamespace(metadata=SimpleNamespace(name="pod-a"), name="pod-a")],
63
+ )
64
+ await view.query_one("#resource_container").mount(table, after=view.panel)
65
+ await pilot.pause()
66
+
67
+ for selector in ("#namespace_select", "#search_input"):
68
+ widget = view.query_one(selector)
69
+ widget.focus()
70
+ await pilot.pause()
71
+
72
+ await pilot.press("tab")
73
+ await pilot.pause()
74
+
75
+ focused = app.focused
76
+ assert isinstance(focused, ListView)
77
+ assert focused.id == "list_view"
78
+
79
+ asyncio.run(_run())
80
+
81
+
39
82
  def test_fetch_resource_returns_none_when_factory_missing(monkeypatch) -> None:
40
83
  app = ResourceHarnessApp()
41
84
  monkeypatch.setattr(ResourceRegistry, "get_factory", lambda _resource_type: None)
@@ -45,10 +88,11 @@ def test_fetch_resource_returns_none_when_factory_missing(monkeypatch) -> None:
45
88
  view = app.view
46
89
  assert view is not None
47
90
 
48
- factory, data, cleaned = view._fetch_resource("pods", None, None)
91
+ factory, data, cleaned, resource_count = view._fetch_resource("pods", None, None)
49
92
  assert factory is None
50
93
  assert data is None
51
94
  assert cleaned == []
95
+ assert resource_count == 0
52
96
 
53
97
  asyncio.run(_run())
54
98
 
@@ -86,10 +130,62 @@ def test_fetch_resource_filters_and_sorts(monkeypatch) -> None:
86
130
  view = app.view
87
131
  assert view is not None
88
132
 
89
- factory, data, cleaned = view._fetch_resource("pods", None, "a")
133
+ factory, data, cleaned, resource_count = view._fetch_resource("pods", None, "a")
90
134
  assert isinstance(factory, FakeFactory)
91
- assert [item.name for item in data.items] == ["a"]
135
+ assert [item.name for item in data.items] == ["b", "a"]
92
136
  assert [item.name for item in cleaned] == ["a"]
137
+ assert resource_count == 1
138
+
139
+ asyncio.run(_run())
140
+
141
+
142
+ def test_apply_resource_recomputes_display_when_keyword_changes() -> None:
143
+ app = ResourceHarnessApp()
144
+
145
+ class FakeFactory:
146
+ resource_type = "pods"
147
+
148
+ def filter(self, raw, keyword):
149
+ return SimpleNamespace(
150
+ items=[item for item in raw.items if keyword in item.name],
151
+ metadata=raw.metadata,
152
+ )
153
+
154
+ def clean(self, raw):
155
+ return [SimpleNamespace(name=item.name) for item in raw.items]
156
+
157
+ async def _run() -> None:
158
+ async with app.run_test(size=(120, 40)):
159
+ view = app.view
160
+ assert view is not None
161
+ view._resource_request_id = 1
162
+ view.keyword = ""
163
+ view._table_resource_type = "pods"
164
+ view.table = SimpleNamespace(raw_data=[], data=[])
165
+ view.panel = SimpleNamespace(resource_count=0)
166
+
167
+ data = SimpleNamespace(
168
+ items=[SimpleNamespace(name="a"), SimpleNamespace(name="b")],
169
+ metadata=SimpleNamespace(_continue=None),
170
+ )
171
+
172
+ view._set_loading = lambda _is_loading: None
173
+ view.refresh_bindings = lambda: None
174
+ view._apply_resource(
175
+ 1,
176
+ "pods",
177
+ None,
178
+ 0,
179
+ FakeFactory(),
180
+ data,
181
+ "a",
182
+ [SimpleNamespace(name="a")],
183
+ 1,
184
+ )
185
+
186
+ assert view.data is data
187
+ assert [item.name for item in view.table.data] == ["a", "b"]
188
+ assert view.panel.resource_count == 2
93
189
 
94
190
  asyncio.run(_run())
95
191
 
@@ -128,7 +224,7 @@ def test_fetch_resource_uses_dynamic_page_size_from_screen_height(monkeypatch) -
128
224
  async with app.run_test(size=(120, 40)):
129
225
  view = app.view
130
226
  assert view is not None
131
- _factory, _data, _cleaned = view._fetch_resource("pods", None, None)
227
+ _factory, _data, _cleaned, _resource_count = view._fetch_resource("pods", None, None)
132
228
 
133
229
  asyncio.run(_run())
134
230
 
@@ -442,19 +538,22 @@ def test_mock_pods_over_200_items_with_pagination_tokens(monkeypatch) -> None:
442
538
  assert view is not None
443
539
  monkeypatch.setattr(view, "_get_page_size", lambda: 100)
444
540
 
445
- _, page1, cleaned1 = view._fetch_resource("pods", None, None)
541
+ _, page1, cleaned1, count1 = view._fetch_resource("pods", None, None)
446
542
  assert len(page1.items) == 100
447
543
  assert len(cleaned1) == 100
544
+ assert count1 == 100
448
545
  assert page1.metadata._continue == "100"
449
546
 
450
- _, page2, cleaned2 = view._fetch_resource("pods", None, None, continue_token=page1.metadata._continue)
547
+ _, page2, cleaned2, count2 = view._fetch_resource("pods", None, None, continue_token=page1.metadata._continue)
451
548
  assert len(page2.items) == 100
452
549
  assert len(cleaned2) == 100
550
+ assert count2 == 100
453
551
  assert page2.metadata._continue == "200"
454
552
 
455
- _, page3, cleaned3 = view._fetch_resource("pods", None, None, continue_token=page2.metadata._continue)
553
+ _, page3, cleaned3, count3 = view._fetch_resource("pods", None, None, continue_token=page2.metadata._continue)
456
554
  assert len(page3.items) == 50
457
555
  assert len(cleaned3) == 50
556
+ assert count3 == 50
458
557
  assert page3.metadata._continue is None
459
558
 
460
559
  asyncio.run(_run())
@@ -469,6 +568,7 @@ def test_mock_pod_pagination_next_and_prev_from_cached_pages() -> None:
469
568
  SimpleNamespace(items=rows, metadata=SimpleNamespace(_continue=next_token)),
470
569
  rows,
471
570
  next_token,
571
+ len(rows),
472
572
  )
473
573
 
474
574
  async def _run() -> None:
@@ -488,6 +588,10 @@ def test_mock_pod_pagination_next_and_prev_from_cached_pages() -> None:
488
588
  view.data = view.resource_pages[key][0][0]
489
589
  view.table = SimpleNamespace(raw_data=[], data=[])
490
590
  view.panel = SimpleNamespace(resource_count=0)
591
+ view.FACTORY_CACHE = SimpleNamespace(
592
+ filter=lambda data, _keyword: data,
593
+ clean=lambda data: data.items,
594
+ )
491
595
 
492
596
  view.action_next_page()
493
597
  assert view.page_index == 1
@@ -516,6 +620,7 @@ def test_pagination_bindings_visibility_changes_by_page() -> None:
516
620
  SimpleNamespace(items=rows, metadata=SimpleNamespace(_continue=next_token)),
517
621
  rows,
518
622
  next_token,
623
+ len(rows),
519
624
  )
520
625
 
521
626
  async def _run() -> None:
@@ -1,2 +0,0 @@
1
- __version__ = "0.2.0b1"
2
- __git_commit__ = ""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes