From f5aeb7abd25090dd7a4b6ba3a3c625a775715a98 Mon Sep 17 00:00:00 2001 From: Jeroen Nijhuis Date: Mon, 15 Jan 2024 21:50:19 +0100 Subject: [PATCH] feat: added support for editing/describing all supported objects --- package-lock.json | 1 + package.json | 1 + src-tauri/src/main.rs | 182 +++++++++++++++++++++++++++ src/components/TabOrchestrator.vue | 21 +++- src/components/tables/pods.ts | 2 +- src/components/tables/types.ts | 45 +++++++ src/providers/TabProvider.ts | 4 + src/services/Kubernetes.ts | 15 +++ src/views/ConfigMaps.vue | 11 +- src/views/CronJobs.vue | 11 +- src/views/Deployments.vue | 11 +- src/views/Describe.vue | 5 +- src/views/Ingresses.vue | 11 +- src/views/Jobs.vue | 11 +- src/views/ObjectEditor.vue | 115 +++++++++++++++-- src/views/PersistentVolumeClaims.vue | 11 +- src/views/Pods.vue | 41 +----- src/views/Secrets.vue | 11 +- src/views/Services.vue | 11 +- src/views/VirtualServices.vue | 15 ++- 20 files changed, 467 insertions(+), 68 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b90320..7c56480 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^3.0.6", + "js-yaml": "^4.1.0", "monaco-editor": "^0.45.0", "monaco-languageclient": "^7.3.0", "radix-vue": "^1.2.7", diff --git a/package.json b/package.json index 0582c8e..c2402f1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^3.0.6", + "js-yaml": "^4.1.0", "monaco-editor": "^0.45.0", "monaco-languageclient": "^7.3.0", "radix-vue": "^1.2.7", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index aa3b457..534887e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -42,6 +42,8 @@ struct SerializableKubeError { impl From for SerializableKubeError { fn from(error: Error) -> Self { + println!("Error: {:?}", error); + match error { Error::Api(api_error) => { let code = api_error.code; @@ -337,6 +339,176 @@ async fn list_persistentvolumeclaims( .map_err(|err| SerializableKubeError::from(err)); } +#[tauri::command] +async fn replace_pod( + context: &str, + namespace: &str, + name: &str, + object: Pod, +) -> Result { + let client = client_with_context(context).await?; + let pod_api: Api = Api::namespaced(client, namespace); + + return pod_api + .replace(name, &Default::default(), &object) + .await + .map(|pod| pod.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_deployment( + context: &str, + namespace: &str, + name: &str, + object: Deployment, +) -> Result { + let client = client_with_context(context).await?; + let deployment_api: Api = Api::namespaced(client, namespace); + + return deployment_api + .replace(name, &Default::default(), &object) + .await + .map(|deployment| deployment.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_job( + context: &str, + namespace: &str, + name: &str, + object: Job, +) -> Result { + let client = client_with_context(context).await?; + let job_api: Api = Api::namespaced(client, namespace); + + return job_api + .replace(name, &Default::default(), &object) + .await + .map(|job| job.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_cronjob( + context: &str, + namespace: &str, + name: &str, + object: CronJob, +) -> Result { + let client = client_with_context(context).await?; + let cronjob_api: Api = Api::namespaced(client, namespace); + + return cronjob_api + .replace(name, &Default::default(), &object) + .await + .map(|cronjob| cronjob.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_configmap( + context: &str, + namespace: &str, + name: &str, + object: ConfigMap, +) -> Result { + let client = client_with_context(context).await?; + let configmap_api: Api = Api::namespaced(client, namespace); + + return configmap_api + .replace(name, &Default::default(), &object) + .await + .map(|configmap| configmap.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_secret( + context: &str, + namespace: &str, + name: &str, + object: Secret, +) -> Result { + let client = client_with_context(context).await?; + let secret_api: Api = Api::namespaced(client, namespace); + + return secret_api + .replace(name, &Default::default(), &object) + .await + .map(|secret| secret.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_service( + context: &str, + namespace: &str, + name: &str, + object: Service, +) -> Result { + let client = client_with_context(context).await?; + let service_api: Api = Api::namespaced(client, namespace); + + return service_api + .replace(name, &Default::default(), &object) + .await + .map(|service| service.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_virtualservice( + context: &str, + namespace: &str, + name: &str, + object: VirtualService, +) -> Result { + let client = client_with_context(context).await?; + let virtualservice_api: Api = Api::namespaced(client, namespace); + + return virtualservice_api + .replace(name, &Default::default(), &object) + .await + .map(|virtualservice| virtualservice.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_ingress( + context: &str, + namespace: &str, + name: &str, + object: Ingress, +) -> Result { + let client = client_with_context(context).await?; + let ingress_api: Api = Api::namespaced(client, namespace); + + return ingress_api + .replace(name, &Default::default(), &object) + .await + .map(|ingress| ingress.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + +#[tauri::command] +async fn replace_persistentvolumeclaim( + context: &str, + namespace: &str, + name: &str, + object: PersistentVolumeClaim, +) -> Result { + let client = client_with_context(context).await?; + let pvc_api: Api = Api::namespaced(client, namespace); + + return pvc_api + .replace(name, &Default::default(), &object) + .await + .map(|pvc: PersistentVolumeClaim| pvc.clone()) + .map_err(|err| SerializableKubeError::from(err)); +} + struct TerminalSession { writer: Arc>>, } @@ -459,6 +631,16 @@ fn main() { list_ingresses, list_persistentvolumes, list_persistentvolumeclaims, + replace_pod, + replace_deployment, + replace_job, + replace_cronjob, + replace_configmap, + replace_secret, + replace_service, + replace_virtualservice, + replace_ingress, + replace_persistentvolumeclaim, create_tty_session, stop_tty_session, write_to_pty diff --git a/src/components/TabOrchestrator.vue b/src/components/TabOrchestrator.vue index bfac4a6..e50615b 100644 --- a/src/components/TabOrchestrator.vue +++ b/src/components/TabOrchestrator.vue @@ -2,6 +2,7 @@ import { TabProviderStateKey, TabProviderCloseTabKey, + TabClosedEvent, } from "@/providers/TabProvider"; import { injectStrict } from "@/lib/utils"; import TabIcon from "@/components/TabIcon.vue"; @@ -68,7 +69,18 @@ const onResizeEnd = () => { window.removeEventListener("mouseup", onResizeEnd); }; -const closeAndSetActiveTab = (id: string) => { +const closeAndSetActiveTab = (id: string, force = false) => { + const canClose = window.dispatchEvent( + new CustomEvent("TabOrchestrator_TabClosed", { + cancelable: true, + detail: { id }, + }) + ); + + if (!canClose && !force) { + return; + } + const indexOfTab = tabs.value.findIndex((tab) => tab.id === id); closeTab(id); @@ -126,7 +138,12 @@ const closeAndSetActiveTab = (id: string) => { :style="{ height: `${tabHeight}px` }" > - + diff --git a/src/components/tables/pods.ts b/src/components/tables/pods.ts index 2e994d0..49c0854 100644 --- a/src/components/tables/pods.ts +++ b/src/components/tables/pods.ts @@ -7,7 +7,7 @@ export const columns: ColumnDef[] = [ accessorKey: "metadata.name", header: "Name", meta: { - class: (row) => { + class: (row: V1Pod) => { return row.status?.phase === "Pending" ? "text-orange-500" : ""; }, }, diff --git a/src/components/tables/types.ts b/src/components/tables/types.ts index 873ba77..ab1a727 100644 --- a/src/components/tables/types.ts +++ b/src/components/tables/types.ts @@ -1,3 +1,6 @@ +import { VirtualService } from "@kubernetes-models/istio/networking.istio.io/v1beta1"; +import { KubernetesObject } from "@kubernetes/client-node"; + export interface BaseRowAction { label: string; } @@ -13,3 +16,45 @@ export interface WithHandler extends BaseRowAction { } export type RowAction = WithOptions | WithHandler; + +export function getDefaultActions( + addTab: any, + context: string +): RowAction[] { + return [ + { + label: "Edit", + handler: (row: T) => { + addTab( + `edit_${row.metadata?.name}`, + `${row.metadata?.name}`, + defineAsyncComponent(() => import("@/views/ObjectEditor.vue")), + { + context: context, + namespace: row.metadata?.namespace, + type: row.kind, + name: row.metadata?.name, + }, + "edit" + ); + }, + }, + { + label: "Describe", + handler: (row) => { + addTab( + `describe_${row.metadata?.name}`, + `${row.metadata?.name}`, + defineAsyncComponent(() => import("@/views/Describe.vue")), + { + context: context, + namespace: row.metadata?.namespace, + type: row.kind, + name: row.metadata?.name, + }, + "describe" + ); + }, + }, + ]; +} diff --git a/src/providers/TabProvider.ts b/src/providers/TabProvider.ts index 5d59145..6db05c6 100644 --- a/src/providers/TabProvider.ts +++ b/src/providers/TabProvider.ts @@ -22,6 +22,10 @@ export const TabProviderAddTabKey: InjectionKey< export const TabProviderCloseTabKey: InjectionKey<(id: string) => void> = Symbol("TabProviderCloseTab"); +export type TabClosedEvent = { + id: string; +}; + export interface Tab { id: string; icon: string; diff --git a/src/services/Kubernetes.ts b/src/services/Kubernetes.ts index a074e55..d2d3c47 100644 --- a/src/services/Kubernetes.ts +++ b/src/services/Kubernetes.ts @@ -59,6 +59,21 @@ export class Kubernetes { }); } + static async replaceObject( + context: string, + namespace: string, + type: string, + name: string, + object: unknown + ): Promise { + return invoke(`replace_${type.toLowerCase()}`, { + context: context, + namespace: namespace, + name: name, + object, + }); + } + static async deletePod( context: string, namespace: string, diff --git a/src/views/ConfigMaps.vue b/src/views/ConfigMaps.vue index 9aaf661..f6d7498 100644 --- a/src/views/ConfigMaps.vue +++ b/src/views/ConfigMaps.vue @@ -15,6 +15,14 @@ import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const configmaps = ref([]); +import { RowAction, getDefaultActions } from "@/components/tables/types"; +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getConfigMaps(refresh: boolean = false) { if (!refresh) { configmaps.value = []; @@ -50,6 +58,5 @@ const { startRefreshing, stopRefreshing } = useDataRefresher( x -@/components/tables/configmaps/configmaps diff --git a/src/views/CronJobs.vue b/src/views/CronJobs.vue index 8a240e8..b43374e 100644 --- a/src/views/CronJobs.vue +++ b/src/views/CronJobs.vue @@ -15,6 +15,14 @@ import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const cronjobs = ref([]); +import { RowAction, getDefaultActions } from "@/components/tables/types"; +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getCronJobs(refresh: boolean = false) { if (!refresh) { cronjobs.value = []; @@ -50,6 +58,5 @@ const { startRefreshing, stopRefreshing } = useDataRefresher( x -@/components/tables/cronjobs/cronjobs diff --git a/src/views/Deployments.vue b/src/views/Deployments.vue index 8c642cc..61b5149 100644 --- a/src/views/Deployments.vue +++ b/src/views/Deployments.vue @@ -8,13 +8,21 @@ import { useToast, ToastAction } from "@/components/ui/toast"; import { KubeContextStateKey } from "@/providers/KubeContextProvider"; const { context, namespace } = injectStrict(KubeContextStateKey); +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + import DataTable from "@/components/ui/DataTable.vue"; +import { RowAction, getDefaultActions } from "@/components/tables/types"; import { columns } from "@/components/tables/deployments"; import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const deployments = ref([]); +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getDeployments(refresh: boolean = false) { if (!refresh) { deployments.value = []; @@ -49,6 +57,5 @@ const { startRefreshing, stopRefreshing } = useDataRefresher( ); -@/components/tables/deployments/deployments diff --git a/src/views/Describe.vue b/src/views/Describe.vue index 8d87df7..6022a13 100644 --- a/src/views/Describe.vue +++ b/src/views/Describe.vue @@ -5,7 +5,8 @@ import { Command } from "@tauri-apps/api/shell"; const props = defineProps<{ context: string; namespace: string; - object: string; + type: string; + name: string; }>(); const describeContents = ref(""); @@ -13,7 +14,7 @@ const describeContents = ref(""); onMounted(() => { const command = new Command("kubectl", [ "describe", - props.object, + `${props.type}/${props.name}`, "--context", props.context, "--namespace", diff --git a/src/views/Ingresses.vue b/src/views/Ingresses.vue index d31cd94..bcca069 100644 --- a/src/views/Ingresses.vue +++ b/src/views/Ingresses.vue @@ -15,6 +15,14 @@ import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const ingresses = ref([]); +import { RowAction, getDefaultActions } from "@/components/tables/types"; +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getIngresses(refresh: boolean = false) { if (!refresh) { ingresses.value = []; @@ -49,6 +57,5 @@ const { startRefreshing, stopRefreshing } = useDataRefresher( ); -@/components/tables/ingresses/ingresses diff --git a/src/views/Jobs.vue b/src/views/Jobs.vue index c0cd422..b11248f 100644 --- a/src/views/Jobs.vue +++ b/src/views/Jobs.vue @@ -15,6 +15,14 @@ import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const jobs = ref([]); +import { RowAction, getDefaultActions } from "@/components/tables/types"; +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getJobs(refresh: boolean = false) { if (!refresh) { jobs.value = []; @@ -49,6 +57,5 @@ const { startRefreshing, stopRefreshing } = useDataRefresher(getJobs, 1000, [ x -@/components/tables/jobs/jobs diff --git a/src/views/ObjectEditor.vue b/src/views/ObjectEditor.vue index d95d1a1..04dbdc6 100644 --- a/src/views/ObjectEditor.vue +++ b/src/views/ObjectEditor.vue @@ -1,21 +1,57 @@ -@/components/monaco/themes/BrillianceBlack diff --git a/src/views/PersistentVolumeClaims.vue b/src/views/PersistentVolumeClaims.vue index 17009b5..2e12ec0 100644 --- a/src/views/PersistentVolumeClaims.vue +++ b/src/views/PersistentVolumeClaims.vue @@ -15,6 +15,14 @@ import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const pvcs = ref([]); +import { RowAction, getDefaultActions } from "@/components/tables/types"; +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getPersistentVolumeClaims(refresh: boolean = false) { if (!refresh) { pvcs.value = []; @@ -50,6 +58,5 @@ const { startRefreshing, stopRefreshing } = useDataRefresher( x -@/components/tables/persistentvolumeclaims/persistentvolumeclaims diff --git a/src/views/Pods.vue b/src/views/Pods.vue index fb4185c..f914667 100644 --- a/src/views/Pods.vue +++ b/src/views/Pods.vue @@ -9,7 +9,7 @@ import { useToast, ToastAction } from "@/components/ui/toast"; import { KubeContextStateKey } from "@/providers/KubeContextProvider"; import DataTable from "@/components/ui/DataTable.vue"; -import { RowAction } from "@/components/tables/types"; +import { RowAction, getDefaultActions } from "@/components/tables/types"; import { columns } from "@/components/tables/pods"; import { useDataRefresher } from "@/composables/refresher"; import { TabProviderAddTabKey } from "@/providers/TabProvider"; @@ -18,43 +18,11 @@ const { context, namespace } = injectStrict(KubeContextStateKey); const addTab = injectStrict(TabProviderAddTabKey); const { toast } = useToast(); -const router = useRouter(); const pods = ref([]); const rowActions: RowAction[] = [ - { - label: "Edit", - handler: (row) => { - addTab( - `edit_${row.metadata?.name}`, - `${row.metadata?.name}`, - defineAsyncComponent(() => import("@/views/ObjectEditor.vue")), - { - context: context.value, - namespace: namespace.value, - object: `pods/${row.metadata?.name}`, - }, - "edit" - ); - }, - }, - { - label: "Describe", - handler: (row) => { - addTab( - `describe_${row.metadata?.name}`, - `${row.metadata?.name}`, - defineAsyncComponent(() => import("@/views/Describe.vue")), - { - context: context.value, - namespace: namespace.value, - object: `pods/${row.metadata?.name}`, - }, - "describe" - ); - }, - }, + ...getDefaultActions(addTab, context.value), { label: "Shell", options: (row) => { @@ -67,7 +35,7 @@ const rowActions: RowAction[] = [ defineAsyncComponent(() => import("@/views/Shell.vue")), { context: context.value, - namespace: namespace.value, + namespace: row.metadata?.namespace ?? namespace.value, pod: row, container: container, }, @@ -86,7 +54,7 @@ const rowActions: RowAction[] = [ defineAsyncComponent(() => import("@/views/LogViewer.vue")), { context: context.value, - namespace: namespace.value, + namespace: row.metadata?.namespace ?? namespace.value, pod: row.metadata?.name, }, "logs" @@ -147,7 +115,6 @@ async function getPods(refresh: boolean = false) { } const rowClasses = (row: V1Pod) => { - // terminating if (row.metadata?.deletionTimestamp) { return "bg-red-500"; } diff --git a/src/views/Secrets.vue b/src/views/Secrets.vue index 7726fc1..a085773 100644 --- a/src/views/Secrets.vue +++ b/src/views/Secrets.vue @@ -15,6 +15,14 @@ import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const secrets = ref([]); +import { RowAction, getDefaultActions } from "@/components/tables/types"; +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getConfigMaps(refresh: boolean = false) { if (!refresh) { secrets.value = []; @@ -50,6 +58,5 @@ const { startRefreshing, stopRefreshing } = useDataRefresher( x -@/components/tables/secrets/secrets diff --git a/src/views/Services.vue b/src/views/Services.vue index a8a29c2..5750789 100644 --- a/src/views/Services.vue +++ b/src/views/Services.vue @@ -15,6 +15,14 @@ import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const services = ref([]); +import { RowAction, getDefaultActions } from "@/components/tables/types"; +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getServices(refresh: boolean = false) { if (!refresh) { services.value = []; @@ -49,6 +57,5 @@ const { startRefreshing, stopRefreshing } = useDataRefresher( ); -@/components/tables/services/services diff --git a/src/views/VirtualServices.vue b/src/views/VirtualServices.vue index 8fe11ca..8b8ba32 100644 --- a/src/views/VirtualServices.vue +++ b/src/views/VirtualServices.vue @@ -15,6 +15,14 @@ import { useDataRefresher } from "@/composables/refresher"; const { toast } = useToast(); const virtualServices = ref([]); +import { RowAction, getDefaultActions } from "@/components/tables/types"; +import { TabProviderAddTabKey } from "@/providers/TabProvider"; +const addTab = injectStrict(TabProviderAddTabKey); + +const rowActions: RowAction[] = [ + ...getDefaultActions(addTab, context.value), +]; + async function getVirtualServices(refresh: boolean = false) { if (!refresh) { virtualServices.value = []; @@ -49,6 +57,9 @@ const { startRefreshing, stopRefreshing } = useDataRefresher( ); -@/components/tables/virtualservices/virtualservices