diff --git a/package-lock.json b/package-lock.json index 9c13df6..2a1f41d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@vueuse/core": "^10.7.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "date-fns": "^3.0.6", "radix-vue": "^1.2.5", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", @@ -3420,6 +3421,22 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/concurrently/node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -3706,19 +3723,12 @@ } }, "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.0.6.tgz", + "integrity": "sha512-W+G99rycpKMMF2/YD064b2lE7jJGUe+EjOES7Q8BIGY8sbNdbgcs9XFTZwvzc9Jx1f3k7LB7gZaZa7f8Agzljg==", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/de-indent": { @@ -15037,6 +15047,17 @@ "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" + }, + "dependencies": { + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.21.0" + } + } } }, "config-chain": { @@ -15241,13 +15262,9 @@ } }, "date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.21.0" - } + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.0.6.tgz", + "integrity": "sha512-W+G99rycpKMMF2/YD064b2lE7jJGUe+EjOES7Q8BIGY8sbNdbgcs9XFTZwvzc9Jx1f3k7LB7gZaZa7f8Agzljg==" }, "de-indent": { "version": "1.0.2", diff --git a/package.json b/package.json index facd11e..fe1ec26 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@vueuse/core": "^10.7.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "date-fns": "^3.0.6", "radix-vue": "^1.2.5", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 83ec990..5b77999 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,8 +1,8 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use tauri::api::shell; -use tauri::{CustomMenuItem, Manager, Menu, Submenu}; +use k8s_openapi::api::batch::v1::Job; +use tauri::Manager; use k8s_openapi::api::apps::v1::Deployment; use k8s_openapi::api::core::v1::{Namespace, Pod, Service}; @@ -185,6 +185,18 @@ async fn list_services( .map_err(|err| SerializableKubeError::from(err)); } +#[tauri::command] +async fn list_jobs(context: &str, namespace: &str) -> Result, SerializableKubeError> { + let client = client_with_context(context).await?; + let jobs_api: Api = Api::namespaced(client, namespace); + + return jobs_api + .list(&ListParams::default()) + .await + .map(|jobs| jobs.items) + .map_err(|err| SerializableKubeError::from(err)); +} + struct TerminalSession { writer: Arc>>, } @@ -298,6 +310,7 @@ fn main() { get_pod, list_deployments, list_services, + list_jobs, create_tty_session, stop_tty_session, write_to_pty diff --git a/src/App.vue b/src/App.vue index e5ec3c0..ed426d8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,20 +12,20 @@ import CommandPaletteProvider from "./providers/CommandPaletteProvider"; diff --git a/src/command-palette/SwitchContext.ts b/src/command-palette/SwitchContext.ts index d9d5877..f1e95a8 100644 --- a/src/command-palette/SwitchContext.ts +++ b/src/command-palette/SwitchContext.ts @@ -13,11 +13,15 @@ export function SwitchContext( contextCache["contexts"] = contexts; contexts.map((context) => { - Kubernetes.getNamespaces(context).then((namespaces) => { - namespaceCache[context] = namespaces.map( - (namespace) => namespace.metadata?.name || "" - ); - }); + Kubernetes.getNamespaces(context) + .then((namespaces) => { + namespaceCache[context] = namespaces.map( + (namespace) => namespace.metadata?.name || "" + ); + }) + .catch((err) => { + namespaceCache[context] = []; + }); }); }); @@ -30,6 +34,15 @@ export function SwitchContext( return { name: context, commands: async () => { + if (namespaceCache[context].length === 0) { + return [ + { + name: "No namespaces found", + execute: () => {}, + }, + ]; + } + return namespaceCache[context].map((namespace) => { return { name: namespace, diff --git a/src/components/tables/deployments/columns.ts b/src/components/tables/deployments/columns.ts index e0d61b8..e0cef6a 100644 --- a/src/components/tables/deployments/columns.ts +++ b/src/components/tables/deployments/columns.ts @@ -1,3 +1,4 @@ +import { formatDateTimeDifference } from "@/lib/utils"; import { V1Deployment } from "@kubernetes/client-node"; import { ColumnDef } from "@tanstack/vue-table"; @@ -13,9 +14,6 @@ export const columns: ColumnDef[] = [ const total = row.status?.replicas || 0; return `${ready}/${total}`; }, - meta: { - class: "text-right", - }, }, { header: "Up-to-date", @@ -24,24 +22,17 @@ export const columns: ColumnDef[] = [ const total = row.status?.replicas || 0; return `${ready}/${total}`; }, - meta: { - class: "text-right", - }, }, { header: "Available", accessorKey: "status.availableReplicas", - meta: { - class: "text-right", - }, }, { header: "Age", - accessorFn: (row) => { - const date = new Date(row.metadata?.creationTimestamp || ""); - return `${Math.floor( - (Date.now() - date.getTime()) / 1000 / 60 / 60 / 24 - )}d`; - }, + accessorFn: (row) => + formatDateTimeDifference( + row.metadata?.creationTimestamp || new Date(), + new Date() + ), }, ]; diff --git a/src/components/tables/jobs/columns.ts b/src/components/tables/jobs/columns.ts new file mode 100644 index 0000000..f62387d --- /dev/null +++ b/src/components/tables/jobs/columns.ts @@ -0,0 +1,32 @@ +import { V1Job } from "@kubernetes/client-node"; +import { ColumnDef } from "@tanstack/vue-table"; +import { formatDateTimeDifference } from "@/lib/utils"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "metadata.name", + header: "Name", + }, + { + header: "Completions", + accessorFn: (row) => { + return `${row.status?.succeeded ?? 0}/${row.spec?.completions}`; + }, + }, + { + header: "Duration", + accessorFn: (row) => + formatDateTimeDifference( + row.status?.startTime || new Date(), + row.status?.completionTime || new Date() + ), + }, + { + header: "Age", + accessorFn: (row) => + formatDateTimeDifference( + row.metadata?.creationTimestamp || new Date(), + new Date() + ), + }, +]; diff --git a/src/components/tables/pods/columns.ts b/src/components/tables/pods/columns.ts index 1481fe3..503b557 100644 --- a/src/components/tables/pods/columns.ts +++ b/src/components/tables/pods/columns.ts @@ -12,9 +12,6 @@ export const columns: ColumnDef[] = [ `${row.status?.containerStatuses?.reduce((acc, curr) => { return curr.ready ? acc + 1 : acc; }, 0)} / ${row.status?.containerStatuses?.length}`, - meta: { - class: "text-right", - }, }, { header: "Restarts", @@ -22,9 +19,6 @@ export const columns: ColumnDef[] = [ `${row.status?.containerStatuses?.reduce((acc, curr) => { return acc + curr.restartCount; }, 0)}`, - meta: { - class: "text-right", - }, }, { header: "Status", diff --git a/src/components/tables/services/columns.ts b/src/components/tables/services/columns.ts index a5cfc0b..f740647 100644 --- a/src/components/tables/services/columns.ts +++ b/src/components/tables/services/columns.ts @@ -1,3 +1,4 @@ +import { formatDateTimeDifference } from "@/lib/utils"; import { V1Deployment, V1Service } from "@kubernetes/client-node"; import { ColumnDef } from "@tanstack/vue-table"; @@ -30,11 +31,10 @@ export const columns: ColumnDef[] = [ }, { header: "Age", - accessorFn: (row) => { - const date = new Date(row.metadata?.creationTimestamp || ""); - return `${Math.floor( - (Date.now() - date.getTime()) / 1000 / 60 / 60 / 24 - )}d`; - }, + accessorFn: (row) => + formatDateTimeDifference( + row.metadata?.creationTimestamp || new Date(), + new Date() + ), }, ]; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bb33eb5..e29d4fb 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; -import { camelize, getCurrentInstance, toHandlerKey } from "vue"; +import { differenceInSeconds } from "date-fns"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -13,3 +13,38 @@ export function injectStrict(key: InjectionKey, fallback?: T) { } return resolved; } + +export function formatDateTimeDifference(startDate: Date, endDate: Date) { + let remainingSeconds = differenceInSeconds(endDate, startDate); + + const days = Math.floor(remainingSeconds / (3600 * 24)); + remainingSeconds -= days * 3600 * 24; + + const hours = Math.floor(remainingSeconds / 3600); + remainingSeconds -= hours * 3600; + + const minutes = Math.floor(remainingSeconds / 60); + remainingSeconds -= minutes * 60; + + const seconds = remainingSeconds; + + // Construct the formatted string + let formattedDuration = ""; + if (days > 0) { + formattedDuration += `${days}d`; + if (hours > 0) { + formattedDuration += `${hours}h`; + } + } else if (hours > 0) { + formattedDuration += `${hours}h`; + if (minutes > 0) { + formattedDuration += `${minutes}m`; + } + } else if (minutes > 0) { + formattedDuration += `${minutes}m`; + } else { + formattedDuration += `${seconds}s`; + } + + return formattedDuration; +} diff --git a/src/router.ts b/src/router.ts index 8d07b11..caba730 100644 --- a/src/router.ts +++ b/src/router.ts @@ -11,6 +11,11 @@ const routes: Array = [ name: "Deployments", component: () => import("./views/Deployments.vue"), }, + { + path: "/jobs", + name: "Jobs", + component: () => import("./views/Jobs.vue"), + }, { path: "/services", name: "Services", diff --git a/src/services/Kubernetes.ts b/src/services/Kubernetes.ts index 14bfd26..d4e1776 100644 --- a/src/services/Kubernetes.ts +++ b/src/services/Kubernetes.ts @@ -1,5 +1,6 @@ import { V1Deployment, + V1Job, V1Namespace, V1Pod, V1Service, @@ -71,4 +72,11 @@ export class Kubernetes { namespace: namespace, }); } + + static async getJobs(context: string, namespace: string): Promise { + return invoke("list_jobs", { + context: context, + namespace: namespace, + }); + } } diff --git a/src/views/Jobs.vue b/src/views/Jobs.vue new file mode 100644 index 0000000..71b4925 --- /dev/null +++ b/src/views/Jobs.vue @@ -0,0 +1,53 @@ + +x +