Skip to content

Commit

Permalink
feat: add functionality to get logs for jobs (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhu2000 authored Mar 27, 2023
1 parent 5025739 commit ee569d0
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 53 deletions.
1 change: 1 addition & 0 deletions app/kube-knots/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ fn main() {
// jobs
workloads::jobs::create_job,
workloads::jobs::delete_job,
workloads::jobs::get_job_logs,
workloads::jobs::get_jobs,
workloads::jobs::update_job,
// pod metrics
Expand Down
36 changes: 32 additions & 4 deletions app/kube-knots/src-tauri/src/workloads/jobs.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use k8s_openapi::api::batch::v1::Job;
use kube::core::ObjectList;
use k8s_openapi::api::{batch::v1::Job, core::v1::Pod};
use kube::{api::ListParams, core::ObjectList, Api};

use crate::internal::resources::{
create_resource, delete_resource, get_resources, update_resource,
use crate::internal::{
client::get_resource_api,
resources::{create_resource, delete_resource, get_resources, update_resource},
};

use super::pods::get_pod_logs;

#[tauri::command]
pub async fn get_jobs(
context: Option<String>,
Expand Down Expand Up @@ -36,3 +39,28 @@ pub async fn delete_job(
) -> Result<bool, String> {
return delete_resource::<Job>(context, namespace, name).await;
}

#[tauri::command]
pub async fn get_job_logs(
context: Option<String>,
namespace: Option<String>,
job_name: String,
) -> Result<String, String> {
let api: Api<Pod> = get_resource_api(context.clone(), namespace.clone()).await?;
let mut lp = ListParams::default();
lp.label_selector = Some(format!("job-name={}", job_name));

let result = api.list(&lp).await.unwrap();

let pod = result.items.get(0);

if pod.is_none() {
return Ok("No pod found for job".to_string());
}

let pod = pod.unwrap();

let pod_name = pod.metadata.name.clone().unwrap();

return get_pod_logs(context, namespace, pod_name, None).await;
}
119 changes: 72 additions & 47 deletions app/kube-knots/src/components/pod-logs.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Listbox } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { type V1Pod } from "@kubernetes/client-node";
import type { V1Pod, V1Job } from "@kubernetes/client-node";
import { useQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api";
import { type ChangeEvent, useCallback, useEffect, useState } from "react";
import { toast } from "react-toastify";

import { useScrollBottom } from "../hooks/use-scroll-bottom";
import { useCurrentContext } from "../providers/current-context-provider";
Expand All @@ -12,14 +13,22 @@ import { ToggleButton } from "./base/toggle-button";

interface PodLogsProps {
isOpen: boolean;
selectedPod: V1Pod | null;
selected: V1Job | V1Pod | null;
handleClose: () => void;
}

export function PodLogs({ isOpen, selectedPod, handleClose }: PodLogsProps) {
const podName = selectedPod?.metadata?.name;
const namespace = selectedPod?.metadata?.namespace;
const [container, setContainer] = useState(selectedPod?.spec?.containers[0].name);
function isPod(resource: V1Job | V1Pod | null): resource is V1Pod {
return (resource as V1Pod)?.kind === "Pod";
}

export function PodLogs({ isOpen, selected, handleClose }: PodLogsProps) {
const name = selected?.metadata?.name;
const namespace = selected?.metadata?.namespace;

const containerName =
selected && isPod(selected) ? selected?.spec?.containers[0].name : undefined;

const [container, setContainer] = useState(containerName);
const { currentContext } = useCurrentContext();

const [followLogs, setFollowLogs] = useState(true);
Expand All @@ -33,69 +42,85 @@ export function PodLogs({ isOpen, selectedPod, handleClose }: PodLogsProps) {
}, []);

useEffect(() => {
setContainer(selectedPod?.spec?.containers[0].name);
setContainer(isPod(selected) ? selected?.spec?.containers[0].name : undefined);
setSearch("");
}, [selectedPod]);
}, [selected]);

const command = isPod(selected) ? "get_pod_logs" : "get_job_logs";

const result = useQuery(
["pod-logs", currentContext, namespace, podName, container],
[command, currentContext, namespace, name, container],
() => {
return invoke<string>("get_pod_logs", {
podName: podName,
const sharedArgs = {
namespace,
container,
context: currentContext,
});
};

const args = isPod(selected)
? { container, podName: name }
: { jobName: selected?.metadata?.labels?.["job-name"] };

return invoke<string>(command, { ...sharedArgs, ...args });
},
{ enabled: !!podName, refetchInterval: 1000 }
{
enabled: !!name,
onError: (error) => toast.error(error as string),
retry: 1,
}
);

const data = result.data?.split("\n") ?? [];

const filteredData = data.filter((item) => {
let filteredData = data.filter((item) => {
return item.toLowerCase().includes(search.toLowerCase());
});

if ((filteredData.length === 1 && filteredData[0] === "") || filteredData.length === 0) {
filteredData = ["Waiting for logs..."];
}

const logBottomRef = useScrollBottom([result.data, followLogs]);

return (
<Drawer
isOpen={isOpen}
handleClose={handleClose}
title={selectedPod?.metadata?.name ?? ""}
title={selected?.metadata?.name ?? ""}
description={
<div className="flex items-center gap-4">
<Listbox value={container} onChange={(e) => setContainer(e)}>
<div className="relative z-10 w-60">
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-gray-100 py-2 pl-3 pr-10 text-left shadow-md dark:bg-gray-800">
<span className="block truncate">{container}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>

<Listbox.Options className="absolute mt-1 w-full overflow-auto rounded-md bg-gray-100 text-gray-800 shadow-lg dark:bg-gray-900 dark:text-gray-100">
{selectedPod?.spec?.containers.map((container, idx) => (
<Listbox.Option
key={idx}
className={`relative cursor-pointer select-none py-2 pl-10 pr-4 hover:bg-gray-200 dark:hover:bg-gray-800`}
value={container.name}
>
{({ selected }) => (
<>
<span className={`block truncate`}>{container.name}</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
{isPod(selected) && (
<Listbox value={container} onChange={(e) => setContainer(e)}>
<div className="relative z-10 w-60">
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-gray-100 py-2 pl-3 pr-10 text-left shadow-md dark:bg-gray-800">
<span className="block truncate">{container}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>

<Listbox.Options className="absolute mt-1 w-full overflow-auto rounded-md bg-gray-100 text-gray-800 shadow-lg dark:bg-gray-900 dark:text-gray-100">
{selected?.spec?.containers.map((container, idx) => (
<Listbox.Option
key={idx}
className={`relative cursor-pointer select-none py-2 pl-10 pr-4 hover:bg-gray-200 dark:hover:bg-gray-800`}
value={container.name}
>
{({ selected }) => (
<>
<span className={`block truncate`}>{container.name}</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
)}

<ToggleButton
checked={followLogs}
Expand Down
2 changes: 1 addition & 1 deletion app/kube-knots/src/components/resource-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function ResourceTable<T extends ResourceBase>({

<Suspense fallback={<div>Loading Logs</div>}>
{actions.includes("logs") && (
<PodLogs isOpen={action === "logs"} handleClose={handleClose} selectedPod={selected} />
<PodLogs isOpen={action === "logs"} handleClose={handleClose} selected={selected} />
)}
</Suspense>
</QueryWrapper>
Expand Down
2 changes: 1 addition & 1 deletion app/kube-knots/src/workloads/jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function Jobs() {
<ResourceTable<V1Job>
command="get_jobs"
headers={["Name", "Image", "Last Run"]}
actions={["edit", "delete"]}
actions={["logs", "edit", "delete"]}
renderData={(item) => {
return (
<>
Expand Down

0 comments on commit ee569d0

Please sign in to comment.