kop-cli 0.2.0b2__tar.gz → 0.3.0__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.
- kop_cli-0.3.0/CHANGELOG.md +11 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/PKG-INFO +4 -4
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/README.md +1 -1
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Pods.md +18 -0
- kop_cli-0.3.0/docs/images/guide/transfer.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/index.md +1 -1
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/tutorial.md +6 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/pyproject.toml +2 -2
- kop_cli-0.3.0/src/kop/__init__.py +2 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/controllers/handler.py +26 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/factory.py +6 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/provider/logs.py +47 -10
- kop_cli-0.3.0/src/kop/provider/transfer.py +325 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Modals.py +10 -10
- kop_cli-0.3.0/src/kop/widgets/Transfer.py +384 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_log_controller.py +24 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_pod_logs.py +33 -23
- kop_cli-0.2.0b2/src/kop/__init__.py +0 -2
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/.gitignore +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/.python-version +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/LICENSE +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/NOTICE +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/__init__.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/faq.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/getting_started.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/ClusterRoleBindings.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/ClusterRoles.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Clusters.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/ConfigMaps.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/CronJobs.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/DaemonSets.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Deployments.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/EndpointSlices.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Endpoints.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/IngressClasses.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Ingresses.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Jobs.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Namespaces.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/NetworkPolicies.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Nodes.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/PersistentVolumeClaims.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/PersistentVolumes.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/RoleBindings.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Roles.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Secrets.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/ServiceAccounts.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/Services.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/StatefulSets.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/StorageClasses.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/index.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide/zh/pods-zh.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/guide.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/guide/create_resource.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/guide/edit_resource.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/guide/forward_port.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/guide/pod_logs.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/guide/resource_detail.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/guide/scale_resource.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/guide/search_kinds.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/guide/select_namespace.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/sample.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/action_workspace.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/add_new_cluster.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/cluster_area.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/detail_view.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/keys.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/resource_view.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/startup.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/startup_without_cluster.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/images/tutorial/themes.png +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/docs/readme_ch.md +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/mkdocs.yml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/event.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/forward.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/kube_client.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/mock_resource_view_deployment.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/mock_resource_view_pod.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/multiple_select.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/pixels.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/pod_logs.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/rich_detail.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/set_release_version.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/scripts/view_pod_logs.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/app/__init__.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/app/main.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/models.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/provider/attach.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/provider/client.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/provider/config.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/provider/events.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/provider/exec.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/provider/forward.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/provider/utils.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/registry.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/renderers/details.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/renderers/fields.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/renderers/formatter.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/renderers/forms.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/renderers/table.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/clusterrolebindings.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/clusterroles.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/configmaps.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/cronjobs.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/daemonsets.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/deployments.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/endpointslices.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/ingressclasses.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/ingresses.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/jobs.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/namespaces.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/networkpolicies.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/node-shell-pod.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/persistentvolumeclaims.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/persistentvolumes.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/pods.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/rolebindings.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/roles.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/secrets.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/serviceaccounts.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/services.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/statefulsets.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/templates/resource/storageclasses.yaml +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/validations.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/views/ActionWorkspace.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/views/EditView.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/views/PodAttach.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/views/PodLog.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/views/PodTerminal.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/views/ResourceView.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/views/StartupView.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Actions.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Attach.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Columns.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Detail.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Directory.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Dynamic.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Edit.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Events.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Expandable.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Focusable.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Forward.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Log.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/MultipleSelect.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Panel.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Pty.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/RichDetail.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/Rules.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/SideMenu.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/src/kop/widgets/__init__.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_action_workspace.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_detail_modal_renderer.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_edit_view.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_event_service.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_pagination_integration.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_resource_view.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_startup_view.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/tests/test_table_renderer.py +0 -0
- {kop_cli-0.2.0b2 → kop_cli-0.3.0}/uv.lock +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## New Features
|
|
2
|
+
|
|
3
|
+
- Transfer files beteewn local and pod
|
|
4
|
+
|
|
5
|
+
## Bug fixes
|
|
6
|
+
|
|
7
|
+
- fixed Kubernetes Client v36.0.0 where the read_namespaced_pod_log function did not support the watch parameter.
|
|
8
|
+
|
|
9
|
+
## Docs
|
|
10
|
+
|
|
11
|
+
- Update on the introduction and usage of transfer files
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kop-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Terminal-based kubernetes operation platform
|
|
5
5
|
Author-email: vegaoqiang <vegaoqiang@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -26,9 +26,9 @@ Classifier: Topic :: Software Development
|
|
|
26
26
|
Classifier: Topic :: System :: Systems Administration
|
|
27
27
|
Classifier: Topic :: Terminals
|
|
28
28
|
Requires-Python: >=3.9
|
|
29
|
-
Requires-Dist: kubernetes
|
|
29
|
+
Requires-Dist: kubernetes==33.1.0
|
|
30
30
|
Requires-Dist: pyte
|
|
31
|
-
Requires-Dist: textual
|
|
31
|
+
Requires-Dist: textual==8.2.7
|
|
32
32
|
Requires-Dist: tree-sitter-bash
|
|
33
33
|
Requires-Dist: tree-sitter-json
|
|
34
34
|
Requires-Dist: tree-sitter-yaml
|
|
@@ -112,7 +112,7 @@ In this mode, the file path and kubeconfig validity are verified, and after vali
|
|
|
112
112
|
|
|
113
113
|
## Operations and Shortcuts
|
|
114
114
|
See demo video:
|
|
115
|
-
[](https://www.youtube.com/watch?v=6L1jOtYvKYg)
|
|
116
116
|
|
|
117
117
|
Please refer to the [Documentation](https://vegaoqiang.github.io/kop/) for more detailed usage instructions.
|
|
118
118
|
|
|
@@ -72,7 +72,7 @@ In this mode, the file path and kubeconfig validity are verified, and after vali
|
|
|
72
72
|
|
|
73
73
|
## Operations and Shortcuts
|
|
74
74
|
See demo video:
|
|
75
|
-
[](https://www.youtube.com/watch?v=6L1jOtYvKYg)
|
|
76
76
|
|
|
77
77
|
Please refer to the [Documentation](https://vegaoqiang.github.io/kop/) for more detailed usage instructions.
|
|
78
78
|
|
|
@@ -10,6 +10,7 @@ Available shortcuts for Pod-related operations are shown in the Footer area at t
|
|
|
10
10
|
| `c` | Create Pods |
|
|
11
11
|
| `d` | Delete Pod |
|
|
12
12
|
| `e` | Edit Pod |
|
|
13
|
+
| `t` | Transfer files |
|
|
13
14
|
| `f` | Port Forward |
|
|
14
15
|
| `l` | Pod Logs |
|
|
15
16
|
| `n` | Next Page |
|
|
@@ -84,6 +85,23 @@ Edits the Pod YAML configuration and submits changes.
|
|
|
84
85
|
- If an update fails, adjust based on the error message and retry.
|
|
85
86
|
- `Edit Pod` opens in the `Action Workspace`.
|
|
86
87
|
|
|
88
|
+
### Transfer Files
|
|
89
|
+
Transfer files allows users to transfer files between their local machine and their pod.
|
|
90
|
+
|
|
91
|
+
> The `Transfer files` function requires the container to contain the `tar` utility; otherwise, it will not be able to transfer directories and only supports transferring single files.
|
|
92
|
+
|
|
93
|
+
**Steps:**
|
|
94
|
+
|
|
95
|
+
1. In the left navigation, move the cursor to `Pods` or enter the `Pods` resource page.
|
|
96
|
+
2. Move the cursor to the target pod
|
|
97
|
+
3. Press the `t` key to open the Transfer dialog box.(If your pod contains multiple containers, please select one container.)
|
|
98
|
+
4. In the `Transfer` dialog box, select the source file and destination path for this transfer.
|
|
99
|
+
5. Click the `Transfer` button to start the transfer.
|
|
100
|
+
|
|
101
|
+

|
|
102
|
+
|
|
103
|
+
> `Transfer` supports custom target filenames
|
|
104
|
+
|
|
87
105
|
### Port Forward
|
|
88
106
|
Forwards a local port to a Pod port so you can access container services from your machine.
|
|
89
107
|
|
|
Binary file
|
|
@@ -7,7 +7,7 @@ hide:
|
|
|
7
7
|
|
|
8
8
|
`kop` is a terminal-based (TUI) Kubernetes operations platform. Its goal is to provide an interactive experience similar to desktop cluster management tools, but fully within the terminal command line, aiming to solve the problem of conveniently operating Kubernetes clusters when no desktop environment is available.
|
|
9
9
|
|
|
10
|
-
<iframe width="
|
|
10
|
+
<iframe width="560" height="315" src="https://www.youtube.com/embed/6L1jOtYvKYg?si=jduCsuOJI_GG6xNe" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
|
11
11
|
|
|
12
12
|
## Why kop
|
|
13
13
|
|
|
@@ -61,6 +61,12 @@ Below the header is action tabs; you can use `Ctrl+[` or `ctrl+]` to switch bet
|
|
|
61
61
|
|
|
62
62
|
The middle section shows the current action workspace. Below are the keyboard shortcuts for actions within the workspace.
|
|
63
63
|
|
|
64
|
+
## Transfer Files
|
|
65
|
+
Transfer files allows users to transfer files between their local machine and their pod.
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
Please see the usage instructions: [Transfer Files](guide/Pods.md#transfer-files)
|
|
64
70
|
|
|
65
71
|
## Theme
|
|
66
72
|
Kop uses Textual to build its user interface and includes multiple modern themes. Press `Ctrl+P` in Kop will directly bring up the theme list, allowing you to select any theme.
|
|
@@ -20,6 +20,7 @@ from kop.widgets.Modals import (
|
|
|
20
20
|
NodeShellLoading,
|
|
21
21
|
NodeShellFailed,
|
|
22
22
|
)
|
|
23
|
+
from kop.widgets.Transfer import FileTransferModal
|
|
23
24
|
from kubernetes import client
|
|
24
25
|
from typing import Optional
|
|
25
26
|
from kop.provider.logs import PodLogs
|
|
@@ -572,6 +573,31 @@ class PodActionHandler(BaseActionHandlerMixin):
|
|
|
572
573
|
callback=forward_callback,
|
|
573
574
|
)
|
|
574
575
|
|
|
576
|
+
@staticmethod
|
|
577
|
+
def transfer(action, resource: PodViewModel, app):
|
|
578
|
+
if resource.status != "Running":
|
|
579
|
+
app.notify("Pod is not running", severity="error")
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
def open_transfer(container_name: str) -> None:
|
|
583
|
+
app.push_screen(
|
|
584
|
+
FileTransferModal(
|
|
585
|
+
api_client=app.endpoint.api_client,
|
|
586
|
+
pod_name=resource.name,
|
|
587
|
+
namespace=resource.namespace,
|
|
588
|
+
container_name=container_name,
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if len(resource.containers) == 1:
|
|
593
|
+
container_obj = resource.containers[0].lazy_clean()
|
|
594
|
+
open_transfer(container_name=container_obj.name)
|
|
595
|
+
else:
|
|
596
|
+
app.push_screen(
|
|
597
|
+
Option([cs.lazy_clean().name for cs in resource.containers], action=action.name),
|
|
598
|
+
callback=open_transfer,
|
|
599
|
+
)
|
|
600
|
+
|
|
575
601
|
@staticmethod
|
|
576
602
|
def delete(action, resource: PodViewModel, app):
|
|
577
603
|
def delete_callback(resource) -> None:
|
|
@@ -280,6 +280,12 @@ class PodFacotry(BaseFactory):
|
|
|
280
280
|
tooltip="Pod logs",
|
|
281
281
|
action="log",
|
|
282
282
|
key="l"),
|
|
283
|
+
ActionModel(name="transfer",
|
|
284
|
+
label="Transfer",
|
|
285
|
+
variant="default",
|
|
286
|
+
tooltip="Transfer files",
|
|
287
|
+
action="transfer",
|
|
288
|
+
key="t"),
|
|
283
289
|
ActionModel(name="forward",
|
|
284
290
|
label="Forward",
|
|
285
291
|
variant="default",
|
|
@@ -3,10 +3,39 @@ import threading
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
from typing import Optional
|
|
6
|
-
from kubernetes import watch
|
|
7
6
|
from kubernetes.client import CoreV1Api
|
|
8
7
|
|
|
9
8
|
|
|
9
|
+
class PodLogStream:
|
|
10
|
+
def __init__(self, response) -> None:
|
|
11
|
+
self.response = response
|
|
12
|
+
self._stopped = False
|
|
13
|
+
self._lock = threading.Lock()
|
|
14
|
+
|
|
15
|
+
def stop(self) -> None:
|
|
16
|
+
with self._lock:
|
|
17
|
+
if self._stopped:
|
|
18
|
+
return
|
|
19
|
+
self._stopped = True
|
|
20
|
+
response = self.response
|
|
21
|
+
|
|
22
|
+
close = getattr(response, "close", None)
|
|
23
|
+
if callable(close):
|
|
24
|
+
close()
|
|
25
|
+
release_conn = getattr(response, "release_conn", None)
|
|
26
|
+
if callable(release_conn):
|
|
27
|
+
release_conn()
|
|
28
|
+
|
|
29
|
+
def __iter__(self):
|
|
30
|
+
stream = getattr(self.response, "stream", None)
|
|
31
|
+
if callable(stream):
|
|
32
|
+
yield from stream(decode_content=True)
|
|
33
|
+
return
|
|
34
|
+
data = getattr(self.response, "data", None)
|
|
35
|
+
if data:
|
|
36
|
+
yield data
|
|
37
|
+
|
|
38
|
+
|
|
10
39
|
|
|
11
40
|
|
|
12
41
|
class PodLogs:
|
|
@@ -53,17 +82,17 @@ class PodLogs:
|
|
|
53
82
|
)
|
|
54
83
|
|
|
55
84
|
def watch_logs(self, timestamps: Optional[bool] = None, tail_lines: Optional[int] = 100):
|
|
56
|
-
|
|
85
|
+
response = self.core_api.read_namespaced_pod_log(
|
|
86
|
+
**self._log_params(timestamps=timestamps, follow=True, tail_lines=tail_lines),
|
|
87
|
+
_preload_content=False,
|
|
88
|
+
)
|
|
89
|
+
self.w = log_stream = PodLogStream(response)
|
|
57
90
|
try:
|
|
58
|
-
for line in
|
|
59
|
-
self.core_api.read_namespaced_pod_log,
|
|
60
|
-
**self._log_params(timestamps=timestamps, follow=True, tail_lines=tail_lines),
|
|
61
|
-
):
|
|
91
|
+
for line in log_stream:
|
|
62
92
|
yield line
|
|
63
93
|
finally:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
self.w = None
|
|
94
|
+
log_stream.stop()
|
|
95
|
+
self.w = None
|
|
67
96
|
|
|
68
97
|
|
|
69
98
|
class LogController:
|
|
@@ -93,7 +122,15 @@ class LogController:
|
|
|
93
122
|
def stop(self, wait: bool = True, timeout: float = 0.5) -> None:
|
|
94
123
|
self._stop_event.set()
|
|
95
124
|
if self.pod_logs.w:
|
|
96
|
-
self.pod_logs.w
|
|
125
|
+
stream = self.pod_logs.w
|
|
126
|
+
if wait:
|
|
127
|
+
stream.stop()
|
|
128
|
+
else:
|
|
129
|
+
threading.Thread(
|
|
130
|
+
target=stream.stop,
|
|
131
|
+
daemon=True,
|
|
132
|
+
name="pod-log-stream-stop",
|
|
133
|
+
).start()
|
|
97
134
|
if wait and self._thread and self._thread.is_alive():
|
|
98
135
|
self._thread.join(timeout=timeout)
|
|
99
136
|
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import tarfile
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
from tempfile import TemporaryFile
|
|
8
|
+
from typing import Literal, Optional
|
|
9
|
+
|
|
10
|
+
from kubernetes.client import ApiClient, CoreV1Api
|
|
11
|
+
from kubernetes.stream import stream
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
EndpointKind = Literal["local", "pod"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class TransferEndpoint:
|
|
19
|
+
kind: EndpointKind
|
|
20
|
+
path: Path | PurePosixPath
|
|
21
|
+
is_dir: bool
|
|
22
|
+
container: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def display(self) -> str:
|
|
26
|
+
return f"{self.kind}:{self.path}"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def name(self) -> str:
|
|
30
|
+
name = self.path.name
|
|
31
|
+
return name or str(self.path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class PodFileEntry:
|
|
36
|
+
path: PurePosixPath
|
|
37
|
+
is_dir: bool
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def label(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
self.path == self.path.parent is possible when path is root "/"
|
|
43
|
+
"""
|
|
44
|
+
suffix = "/" if self.is_dir and self.path != self.path.parent else ""
|
|
45
|
+
return f"{self.path.name or '/'}{suffix}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PodFileSystem:
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
api_client: ApiClient,
|
|
52
|
+
pod_name: str,
|
|
53
|
+
namespace: str,
|
|
54
|
+
container_name: Optional[str] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.core_api = CoreV1Api(api_client=api_client)
|
|
57
|
+
self.pod_name = pod_name
|
|
58
|
+
self.namespace = namespace
|
|
59
|
+
self.container_name = container_name
|
|
60
|
+
|
|
61
|
+
def list_dir(self, path: PurePosixPath | str) -> list[PodFileEntry]:
|
|
62
|
+
pod_path = PurePosixPath(str(path) or "/")
|
|
63
|
+
quoted_path = shlex.quote(str(pod_path))
|
|
64
|
+
script = (
|
|
65
|
+
f"p={quoted_path}; "
|
|
66
|
+
'if [ ! -d "$p" ]; then echo "not a directory: $p" >&2; exit 2; fi; '
|
|
67
|
+
'for entry in "$p"/* "$p"/.[!.]* "$p"/..?*; do '
|
|
68
|
+
'[ -e "$entry" ] || continue; '
|
|
69
|
+
'name=${entry##*/}; '
|
|
70
|
+
'if [ -d "$entry" ]; then printf "d\\t%s\\n" "$name"; '
|
|
71
|
+
'else printf "f\\t%s\\n" "$name"; fi; '
|
|
72
|
+
"done"
|
|
73
|
+
)
|
|
74
|
+
output = self._exec(["sh", "-c", script])
|
|
75
|
+
entries: list[PodFileEntry] = []
|
|
76
|
+
for line in output.splitlines():
|
|
77
|
+
if "\t" not in line:
|
|
78
|
+
continue
|
|
79
|
+
kind, name = line.split("\t", 1)
|
|
80
|
+
if not name or name in {".", ".."}:
|
|
81
|
+
continue
|
|
82
|
+
entries.append(PodFileEntry(path=pod_path / name, is_dir=kind == "d"))
|
|
83
|
+
entries.sort(key=lambda item: (not item.is_dir, item.path.name.lower()))
|
|
84
|
+
return entries
|
|
85
|
+
|
|
86
|
+
def _exec(self, command: list[str]) -> str:
|
|
87
|
+
return stream(
|
|
88
|
+
self.core_api.connect_get_namespaced_pod_exec,
|
|
89
|
+
self.pod_name,
|
|
90
|
+
self.namespace,
|
|
91
|
+
command=command,
|
|
92
|
+
container=self.container_name,
|
|
93
|
+
stderr=True,
|
|
94
|
+
stdin=False,
|
|
95
|
+
stdout=True,
|
|
96
|
+
tty=False,
|
|
97
|
+
_preload_content=True,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PodFileTransfer:
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
api_client: ApiClient,
|
|
105
|
+
pod_name: str,
|
|
106
|
+
namespace: str,
|
|
107
|
+
container_name: Optional[str] = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
self.core_api = CoreV1Api(api_client=api_client)
|
|
110
|
+
self.pod_name = pod_name
|
|
111
|
+
self.namespace = namespace
|
|
112
|
+
self.container_name = container_name
|
|
113
|
+
self._command_cache: dict[str, bool] = {}
|
|
114
|
+
|
|
115
|
+
def upload(self, source: Path, dest_dir: PurePosixPath, dest_name: str) -> None:
|
|
116
|
+
if not source.exists():
|
|
117
|
+
raise FileNotFoundError(str(source))
|
|
118
|
+
if not dest_name:
|
|
119
|
+
raise ValueError("Destination name is required")
|
|
120
|
+
|
|
121
|
+
if not self.has_command("tar"):
|
|
122
|
+
self._upload_file_with_cat(source, dest_dir, dest_name)
|
|
123
|
+
return
|
|
124
|
+
self._upload_with_tar(source, dest_dir, dest_name)
|
|
125
|
+
|
|
126
|
+
def download(
|
|
127
|
+
self,
|
|
128
|
+
source: PurePosixPath,
|
|
129
|
+
dest_dir: Path,
|
|
130
|
+
dest_name: str,
|
|
131
|
+
source_is_dir: bool = False,
|
|
132
|
+
) -> None:
|
|
133
|
+
if not dest_name:
|
|
134
|
+
raise ValueError("Destination name is required")
|
|
135
|
+
if not self.has_command("tar"):
|
|
136
|
+
if source_is_dir:
|
|
137
|
+
raise RuntimeError("Directory download requires tar in the container")
|
|
138
|
+
self._download_file_with_cat(source, dest_dir, dest_name)
|
|
139
|
+
return
|
|
140
|
+
self._download_with_tar(source, dest_dir, dest_name)
|
|
141
|
+
|
|
142
|
+
def has_command(self, command: str) -> bool:
|
|
143
|
+
if command in self._command_cache:
|
|
144
|
+
return self._command_cache[command]
|
|
145
|
+
quoted = shlex.quote(command)
|
|
146
|
+
output = stream(
|
|
147
|
+
self.core_api.connect_get_namespaced_pod_exec,
|
|
148
|
+
self.pod_name,
|
|
149
|
+
self.namespace,
|
|
150
|
+
command=["sh", "-c", f"command -v {quoted} >/dev/null 2>&1 && echo yes || echo no"],
|
|
151
|
+
container=self.container_name,
|
|
152
|
+
stderr=False,
|
|
153
|
+
stdin=False,
|
|
154
|
+
stdout=True,
|
|
155
|
+
tty=False,
|
|
156
|
+
_preload_content=True,
|
|
157
|
+
)
|
|
158
|
+
available = str(output).strip() == "yes"
|
|
159
|
+
self._command_cache[command] = available
|
|
160
|
+
return available
|
|
161
|
+
|
|
162
|
+
def _upload_with_tar(self, source: Path, dest_dir: PurePosixPath, dest_name: str) -> None:
|
|
163
|
+
quoted_dir = shlex.quote(str(dest_dir))
|
|
164
|
+
command = ["sh", "-c", f"mkdir -p {quoted_dir} && tar xf - -C {quoted_dir}"]
|
|
165
|
+
resp = self._open_exec(command, stdin=True)
|
|
166
|
+
try:
|
|
167
|
+
with TemporaryFile() as archive:
|
|
168
|
+
with tarfile.open(fileobj=archive, mode="w") as tar:
|
|
169
|
+
tar.add(source, arcname=dest_name, recursive=True)
|
|
170
|
+
archive.seek(0)
|
|
171
|
+
while True:
|
|
172
|
+
chunk = archive.read(1024 * 256)
|
|
173
|
+
if not chunk:
|
|
174
|
+
break
|
|
175
|
+
resp.write_stdin(chunk)
|
|
176
|
+
resp.write_stdin(b"")
|
|
177
|
+
self._drain_response(resp)
|
|
178
|
+
finally:
|
|
179
|
+
resp.close()
|
|
180
|
+
|
|
181
|
+
def _upload_file_with_cat(self, source: Path, dest_dir: PurePosixPath, dest_name: str) -> None:
|
|
182
|
+
if source.is_dir():
|
|
183
|
+
raise RuntimeError("Directory upload requires tar in the container")
|
|
184
|
+
|
|
185
|
+
dest_path = dest_dir / dest_name
|
|
186
|
+
quoted_dir = shlex.quote(str(dest_dir))
|
|
187
|
+
quoted_dest = shlex.quote(str(dest_path))
|
|
188
|
+
command = ["sh", "-c", f"mkdir -p {quoted_dir} && cat > {quoted_dest}"]
|
|
189
|
+
resp = self._open_exec(command, stdin=True)
|
|
190
|
+
try:
|
|
191
|
+
with source.open("rb") as source_file:
|
|
192
|
+
while True:
|
|
193
|
+
chunk = source_file.read(1024 * 256)
|
|
194
|
+
if not chunk:
|
|
195
|
+
break
|
|
196
|
+
resp.write_stdin(chunk)
|
|
197
|
+
self._drain_response(resp)
|
|
198
|
+
finally:
|
|
199
|
+
resp.close()
|
|
200
|
+
|
|
201
|
+
def _download_with_tar(self, source: PurePosixPath, dest_dir: Path, dest_name: str) -> None:
|
|
202
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
parent = str(source.parent) if str(source.parent) else "/"
|
|
204
|
+
source_name = source.name or "."
|
|
205
|
+
command = [
|
|
206
|
+
"sh",
|
|
207
|
+
"-c",
|
|
208
|
+
f"tar cf - -C {shlex.quote(parent)} {shlex.quote(source_name)}",
|
|
209
|
+
]
|
|
210
|
+
resp = self._open_exec(command, stdin=False)
|
|
211
|
+
try:
|
|
212
|
+
archive_data = io.BytesIO()
|
|
213
|
+
empty_reads = 0
|
|
214
|
+
while resp.is_open():
|
|
215
|
+
resp.update(timeout=1)
|
|
216
|
+
stdout = resp.read_channel(1, timeout=0)
|
|
217
|
+
if stdout:
|
|
218
|
+
if isinstance(stdout, str):
|
|
219
|
+
stdout = stdout.encode()
|
|
220
|
+
archive_data.write(stdout)
|
|
221
|
+
empty_reads = 0
|
|
222
|
+
stderr = resp.read_channel(2, timeout=0)
|
|
223
|
+
if stderr:
|
|
224
|
+
raise RuntimeError(self._channel_text(stderr).strip())
|
|
225
|
+
if not stdout:
|
|
226
|
+
empty_reads += 1
|
|
227
|
+
if empty_reads >= 3:
|
|
228
|
+
break
|
|
229
|
+
archive_data.seek(0)
|
|
230
|
+
with tarfile.open(fileobj=archive_data, mode="r:*") as tar:
|
|
231
|
+
self._safe_extract(tar, dest_dir)
|
|
232
|
+
extracted = dest_dir / source_name
|
|
233
|
+
target = dest_dir / dest_name
|
|
234
|
+
if extracted != target and extracted.exists():
|
|
235
|
+
if target.exists():
|
|
236
|
+
raise FileExistsError(str(target))
|
|
237
|
+
os.replace(extracted, target)
|
|
238
|
+
finally:
|
|
239
|
+
resp.close()
|
|
240
|
+
|
|
241
|
+
def _download_file_with_cat(self, source: PurePosixPath, dest_dir: Path, dest_name: str) -> None:
|
|
242
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
target = dest_dir / dest_name
|
|
244
|
+
command = ["sh", "-c", f"cat {shlex.quote(str(source))}"]
|
|
245
|
+
resp = self._open_exec(command, stdin=False)
|
|
246
|
+
try:
|
|
247
|
+
data = self._read_stdout_response(resp)
|
|
248
|
+
target.write_bytes(data)
|
|
249
|
+
finally:
|
|
250
|
+
resp.close()
|
|
251
|
+
|
|
252
|
+
def _open_exec(self, command: list[str], stdin: bool):
|
|
253
|
+
return stream(
|
|
254
|
+
self.core_api.connect_get_namespaced_pod_exec,
|
|
255
|
+
self.pod_name,
|
|
256
|
+
self.namespace,
|
|
257
|
+
command=command,
|
|
258
|
+
container=self.container_name,
|
|
259
|
+
stderr=True,
|
|
260
|
+
stdin=stdin,
|
|
261
|
+
stdout=True,
|
|
262
|
+
tty=False,
|
|
263
|
+
binary=True,
|
|
264
|
+
_preload_content=False,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def _drain_response(self, resp) -> None:
|
|
268
|
+
errors: list[str] = []
|
|
269
|
+
empty_reads = 0
|
|
270
|
+
while resp.is_open():
|
|
271
|
+
resp.update(timeout=1)
|
|
272
|
+
stderr = resp.read_channel(2, timeout=0)
|
|
273
|
+
if stderr:
|
|
274
|
+
errors.append(self._channel_text(stderr))
|
|
275
|
+
stdout = resp.read_channel(1, timeout=0)
|
|
276
|
+
if stderr or stdout:
|
|
277
|
+
empty_reads = 0
|
|
278
|
+
else:
|
|
279
|
+
empty_reads += 1
|
|
280
|
+
if empty_reads >= 3:
|
|
281
|
+
break
|
|
282
|
+
if errors:
|
|
283
|
+
raise RuntimeError("".join(errors).strip())
|
|
284
|
+
|
|
285
|
+
def _read_stdout_response(self, resp) -> bytes:
|
|
286
|
+
output = io.BytesIO()
|
|
287
|
+
errors: list[str] = []
|
|
288
|
+
empty_reads = 0
|
|
289
|
+
while resp.is_open():
|
|
290
|
+
resp.update(timeout=1)
|
|
291
|
+
stdout = resp.read_channel(1, timeout=0)
|
|
292
|
+
if stdout:
|
|
293
|
+
if isinstance(stdout, str):
|
|
294
|
+
stdout = stdout.encode()
|
|
295
|
+
output.write(stdout)
|
|
296
|
+
empty_reads = 0
|
|
297
|
+
stderr = resp.read_channel(2, timeout=0)
|
|
298
|
+
if stderr:
|
|
299
|
+
errors.append(self._channel_text(stderr))
|
|
300
|
+
empty_reads = 0
|
|
301
|
+
if not stdout and not stderr:
|
|
302
|
+
empty_reads += 1
|
|
303
|
+
if empty_reads >= 3:
|
|
304
|
+
break
|
|
305
|
+
if errors:
|
|
306
|
+
raise RuntimeError("".join(errors).strip())
|
|
307
|
+
return output.getvalue()
|
|
308
|
+
|
|
309
|
+
def _channel_text(self, value: bytes | str) -> str:
|
|
310
|
+
if isinstance(value, bytes):
|
|
311
|
+
return value.decode("utf-8", "replace")
|
|
312
|
+
return value
|
|
313
|
+
|
|
314
|
+
def _safe_extract(self, tar: tarfile.TarFile, dest_dir: Path) -> None:
|
|
315
|
+
"""
|
|
316
|
+
Extracts a tar file to the specified destination directory while
|
|
317
|
+
preventing path traversal attacks and local file or directory were
|
|
318
|
+
overwritten by downloaded files
|
|
319
|
+
"""
|
|
320
|
+
base = dest_dir.resolve()
|
|
321
|
+
for member in tar.getmembers():
|
|
322
|
+
target = (dest_dir / member.name).resolve()
|
|
323
|
+
if base not in target.parents and target != base:
|
|
324
|
+
raise RuntimeError(f"Unsafe tar path: {member.name}")
|
|
325
|
+
tar.extractall(dest_dir)
|
|
@@ -567,7 +567,7 @@ class ActionPortForward(ModalScreen):
|
|
|
567
567
|
#remote_port_select {
|
|
568
568
|
width: 100%;
|
|
569
569
|
}
|
|
570
|
-
#cancel, #start, #stop {
|
|
570
|
+
#cancel-forward, #start-forward, #stop-forward {
|
|
571
571
|
width: 100%;
|
|
572
572
|
}
|
|
573
573
|
#action_buttons {
|
|
@@ -620,9 +620,9 @@ class ActionPortForward(ModalScreen):
|
|
|
620
620
|
Select(options=port_options, value=initial_value, allow_blank=False, id="remote_port_select"),
|
|
621
621
|
Label("Open in Browser"),
|
|
622
622
|
Switch(id="open_in_browser", value=True),
|
|
623
|
-
Grid(Button("Cancel", variant="error", id="cancel"),
|
|
624
|
-
Button("Stop", variant="warning", id="stop", disabled=True),
|
|
625
|
-
Button("Start", variant="primary", id="start", disabled=False),
|
|
623
|
+
Grid(Button("Cancel", variant="error", id="cancel-forward"),
|
|
624
|
+
Button("Stop", variant="warning", id="stop-forward", disabled=True),
|
|
625
|
+
Button("Start", variant="primary", id="start-forward", disabled=False),
|
|
626
626
|
id="action_buttons",
|
|
627
627
|
),
|
|
628
628
|
id="dialog",
|
|
@@ -642,14 +642,14 @@ class ActionPortForward(ModalScreen):
|
|
|
642
642
|
def action_start(self) -> None:
|
|
643
643
|
self.on_start_press()
|
|
644
644
|
|
|
645
|
-
@on(Button.Pressed, "#cancel")
|
|
645
|
+
@on(Button.Pressed, "#cancel-forward")
|
|
646
646
|
def on_cancel_press(self, event: Button.Pressed) -> None:
|
|
647
647
|
self.app.pop_screen()
|
|
648
648
|
|
|
649
649
|
def _update_action_buttons(self) -> None:
|
|
650
650
|
remote_port_select = self.query_one("#remote_port_select", Select)
|
|
651
|
-
start_btn = self.query_one("#start", Button)
|
|
652
|
-
stop_btn = self.query_one("#stop", Button)
|
|
651
|
+
start_btn = self.query_one("#start-forward", Button)
|
|
652
|
+
stop_btn = self.query_one("#stop-forward", Button)
|
|
653
653
|
selected = remote_port_select.value
|
|
654
654
|
if selected == Select.NULL:
|
|
655
655
|
start_btn.disabled = True
|
|
@@ -660,7 +660,7 @@ class ActionPortForward(ModalScreen):
|
|
|
660
660
|
start_btn.disabled = is_forwarded or not self._is_local_port_valid()
|
|
661
661
|
stop_btn.disabled = not is_forwarded
|
|
662
662
|
|
|
663
|
-
@on(Button.Pressed, "#start")
|
|
663
|
+
@on(Button.Pressed, "#start-forward")
|
|
664
664
|
def on_start_press(self) -> None:
|
|
665
665
|
local_port_input = self.query_one("#local_port", Input)
|
|
666
666
|
open_in_browser = self.query_one("#open_in_browser", Switch).value
|
|
@@ -689,7 +689,7 @@ class ActionPortForward(ModalScreen):
|
|
|
689
689
|
}
|
|
690
690
|
)
|
|
691
691
|
|
|
692
|
-
@on(Button.Pressed, "#stop")
|
|
692
|
+
@on(Button.Pressed, "#stop-forward")
|
|
693
693
|
def on_stop_press(self) -> None:
|
|
694
694
|
remote_port_select = self.query_one("#remote_port_select", Select)
|
|
695
695
|
selected = remote_port_select.value
|
|
@@ -721,7 +721,7 @@ class ActionPortForward(ModalScreen):
|
|
|
721
721
|
|
|
722
722
|
@on(Input.Submitted, "#local_port")
|
|
723
723
|
def submit_local_port(self) -> None:
|
|
724
|
-
if not self.query_one("#start", Button).disabled:
|
|
724
|
+
if not self.query_one("#start-forward", Button).disabled:
|
|
725
725
|
self.on_start_press()
|
|
726
726
|
|
|
727
727
|
|