Skip to content

Commit

Permalink
feat: added visual cpu and memory usage indicator to pod overview
Browse files Browse the repository at this point in the history
  • Loading branch information
unxsist committed May 9, 2024
1 parent 2a4617b commit ee569bd
Show file tree
Hide file tree
Showing 17 changed files with 410 additions and 22 deletions.
19 changes: 19 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", branch =
portable-pty = "0.8.1"
uuid = "1.4.1"
either = "1.9.0"
k8s-metrics = "0.14.0"

[target.'cfg(target_os = "macos")'.dependencies]
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel" }
Expand Down
32 changes: 32 additions & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use either::Either;
use istio_api_rs::networking::v1beta1::virtual_service::VirtualService;
use k8s_metrics::v1beta1::PodMetrics;
use k8s_openapi::api::batch::v1::{CronJob, Job};
use k8s_openapi::api::networking::v1::Ingress;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIGroup, APIResource};
Expand Down Expand Up @@ -191,6 +192,19 @@ async fn list_pods(
.map_err(|err| SerializableKubeError::from(err));
}


#[tauri::command]
async fn get_pod_metrics(context: &str, namespace: &str) -> Result<Vec<PodMetrics>, SerializableKubeError> {
let client = client_with_context(context).await?;
let metrics_api : Api<PodMetrics> = Api::namespaced(client, namespace);

return metrics_api
.list(&ListParams::default())
.await
.map(|metrics| metrics.items)
.map_err(|err| SerializableKubeError::from(err));
}

#[tauri::command]
async fn get_pod(context: &str, namespace: &str, name: &str) -> Result<Pod, SerializableKubeError> {
let client = client_with_context(context).await?;
Expand Down Expand Up @@ -242,6 +256,22 @@ async fn list_deployments(
.map_err(|err| SerializableKubeError::from(err));
}

#[tauri::command]
async fn restart_deployment(
context: &str,
namespace: &str,
name: &str,
) -> Result<bool, SerializableKubeError> {
let client = client_with_context(context).await?;
let deployment_api: Api<Deployment> = Api::namespaced(client, namespace);

return deployment_api
.restart(name)
.await
.map(|_deployment| true)
.map_err(|err| SerializableKubeError::from(err));
}

#[tauri::command]
async fn list_services(
context: &str,
Expand Down Expand Up @@ -739,6 +769,7 @@ fn main() {
get_pod,
delete_pod,
list_deployments,
restart_deployment,
list_jobs,
list_cronjobs,
list_configmaps,
Expand All @@ -758,6 +789,7 @@ fn main() {
replace_virtualservice,
replace_ingress,
replace_persistentvolumeclaim,
get_pod_metrics,
create_tty_session,
stop_tty_session,
write_to_pty
Expand Down
11 changes: 8 additions & 3 deletions src/components/tables/pods.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { V1Pod } from "@kubernetes/client-node";
import { PodMetric, V1Pod } from "@kubernetes/client-node";
import { ColumnDef } from "@tanstack/vue-table";
import { formatDateTimeDifference } from "@/lib/utils";
import PodUsageChart from "../ui/PodUsageChart.vue";

export const columns: ColumnDef<V1Pod>[] = [
export const columns: ColumnDef<V1Pod & { metrics: PodMetric[] }>[] = [
{
accessorKey: "metadata.name",
header: "Name",
Expand Down Expand Up @@ -37,7 +38,11 @@ export const columns: ColumnDef<V1Pod>[] = [
},
},
{
header: "CPU",
header: "Usage",
size: 100,
cell: ({ row }) => {
return h(PodUsageChart, { pod: row.original });
},
},
{
header: "IP",
Expand Down
5 changes: 1 addition & 4 deletions src/components/tables/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,8 @@ export function getDefaultActions<T extends KubernetesObject | VirtualService>(
console.log(error);
});

command.on("close", () => {
dialog.close();
});

command.spawn();
dialog.close();
},
},
],
Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const table = useVueTable({
v-bind:enable-header-drag-region="true"
v-for="header in headerGroup.headers"
:key="header.id"
:style="{ width: `${header.getSize()}px` }"
>
<FlexRender
v-if="!header.isPlaceholder"
Expand All @@ -86,7 +87,8 @@ const table = useVueTable({
<ContextMenuSub>
<ContextMenuSubTrigger>Columns</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuCheckboxItem :checked="column.getIsVisible()"
<ContextMenuCheckboxItem
:checked="column.getIsVisible()"
v-for="column in table.getAllColumns()"
:key="column.id"
@select="
Expand Down
154 changes: 154 additions & 0 deletions src/components/ui/PodUsageChart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<script setup lang="ts">
import { V1Pod, PodMetric } from "@kubernetes/client-node";
import { memoryParser } from "@/lib/unitParsers";
import PodUsageChartBar from "./PodUsageChartBar.vue";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const props = defineProps<{
pod: V1Pod & { metrics: PodMetric[] };
}>();
const cpuDatapoints = computed(() => {
return props.pod.metrics.map((metric: PodMetric) => {
return metric.containers.reduce((acc, container) => {
return acc + Number(container.usage.cpu.replace("n", "")) / 1000000;
}, 0);
});
});
const memoryDatapoints = computed(() => {
return props.pod.metrics.map((metric: PodMetric) => {
return metric.containers.reduce((acc, container) => {
return acc + Number(container.usage.memory.replace("Ki", "")) * 1000;
}, 0);
});
});
const latestCpuDatapoint = computed(() => {
return cpuDatapoints.value.toReversed()[0];
});
const latestMemoryDatapoint = computed(() => {
return memoryDatapoints.value.toReversed()[0];
});
const cpuRequest = computed(() => {
return props.pod.spec.containers.reduce((acc, container) => {
if (container.resources?.requests?.cpu === undefined) {
return acc;
}
if (!container.resources?.requests?.cpu.includes("m")) {
return acc + Number(container.resources?.requests?.cpu) * 1000;
}
return (
acc + Number(container.resources?.requests?.cpu?.replace("m", "") || 0)
);
}, 0);
});
const memoryRequest = computed(() => {
return props.pod.spec.containers.reduce((acc, container) => {
if (container.resources?.requests?.memory === undefined) {
return acc;
}
return acc + memoryParser(container.resources?.requests?.memory);
return (
acc + Number(container.resources?.requests?.cpu?.replace("Mi", "") || 0)
);
}, 0);
});
const cpuLimit = computed(() => {
return props.pod.spec.containers.reduce((acc, container) => {
if (container.resources?.limits?.cpu === undefined) {
return acc;
}
if (!container.resources?.limits?.cpu.includes("m")) {
return acc + Number(container.resources?.limits?.cpu) * 1000;
}
return (
acc + Number(container.resources?.limits?.cpu?.replace("m", "") || 0)
);
}, 0);
});
const memoryLimit = computed(() => {
return props.pod.spec.containers.reduce((acc, container) => {
if (container.resources?.limits?.memory === undefined) {
return acc;
}
return acc + memoryParser(container.resources?.limits?.memory);
}, 0);
});
const cpuUsage = computed(() => {
return cpuLimit.value > 0
? (latestCpuDatapoint.value / cpuLimit.value) * 100
: 0;
});
const memoryUsage = computed(() => {
return memoryLimit.value > 0
? (latestMemoryDatapoint.value / memoryLimit.value) * 100
: 0;
});
const cpuThreshold = computed(() => {
return cpuLimit.value > 0 ? (cpuRequest.value / cpuLimit.value) * 100 : 0;
});
const memoryThreshold = computed(() => {
return memoryLimit.value > 0
? (memoryRequest.value / memoryLimit.value) * 100
: 0;
});
</script>
<template>
<TooltipProvider :disable-hoverable-content="true">
<Tooltip :delay-duration="200">
<TooltipTrigger as-child>
<div class="space-y-1">
<PodUsageChartBar
v-if="cpuDatapoints"
text="C"
:value="cpuUsage"
:threshold="cpuThreshold > 0 ? cpuThreshold : undefined"
/>
<PodUsageChartBar
v-if="memoryDatapoints"
text="M"
:value="memoryUsage"
:threshold="memoryThreshold > 0 ? memoryThreshold : undefined"
/>
</div>
</TooltipTrigger>
<TooltipContent>
<div>
<div class="font-bold">CPU</div>
<div>
U: {{ latestCpuDatapoint.toFixed(0) }}m / R: {{ cpuRequest }}m / L:
{{ cpuLimit }}m
</div>
<div class="mt-2 font-bold">Memory</div>
<div>
U: {{ (latestMemoryDatapoint / 1000000).toFixed(0) }}Mi / R:
{{ (memoryRequest / 1000000).toFixed(0) }} Mi / L:
{{ (memoryLimit / 1000000).toFixed(0) }}Mi
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
29 changes: 29 additions & 0 deletions src/components/ui/PodUsageChartBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
defineProps<{
text: string;
value: number;
threshold?: number;
}>();
</script>
<template>
<div>
<div class="flex items-center space-x-2 text-xxs">
<span class="w-[10px] text-center leading-none">{{ text }}</span>
<div class="relative w-full h-[5px] bg-gray-800">
<div
class="w-0 h-[5px] bg-green-500"
:class="{
'bg-red-500': value >= 90,
'bg-yellow-500': value >= 70 && value < 90,
}"
:style="{ width: `${value}%` }"
></div>
<div
v-if="threshold"
class="absolute top-0 w-[1px] h-[5px] bg-white opacity-50"
:style="{ left: `${threshold}%` }"
></div>
</div>
</div>
</div>
</template>
14 changes: 14 additions & 0 deletions src/components/ui/tooltip/Tooltip.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import { TooltipRoot, type TooltipRootEmits, type TooltipRootProps, useForwardPropsEmits } from 'radix-vue'
const props = defineProps<TooltipRootProps>()
const emits = defineEmits<TooltipRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>

<template>
<TooltipRoot v-bind="forwarded">
<slot />
</TooltipRoot>
</template>
Loading

0 comments on commit ee569bd

Please sign in to comment.