diff --git a/app/nodes/[node]/index.tsx b/app/nodes/[node]/index.tsx index 3628fc0..9edf425 100644 --- a/app/nodes/[node]/index.tsx +++ b/app/nodes/[node]/index.tsx @@ -1,16 +1,16 @@ import { Link, useSearchParams } from "expo-router"; -import { View, Text, SafeAreaView, ScrollView, Button } from "react-native"; -import { NodeResource, useNode } from "../../../hooks/useNode"; +import { View, Text, ScrollView } from "react-native"; +import { NodeResource, ResourceType, useNode } from "../../../hooks/useNode"; import Icon from "@expo/vector-icons/Feather"; import tw from "twrnc"; -import { formatBytes, formatPercentage } from "../../../lib/helper/format"; -import { useEffect, useMemo } from "react"; +import { formatBytes } from "../../../lib/helper/format"; +import { useMemo } from "react"; import { Gauge } from "../../../components/Gauge"; import ProgressBar from "../../../components/ProgressBar"; import Card from "../../../components/Card"; interface ResourceListItemProps { - type: "LXC" | "QEMU"; + type: ResourceType; resource: NodeResource; } @@ -23,7 +23,7 @@ export function ResourceListItem({ type, resource }: ResourceListItemProps) { resource.status === "running" ? "bg-green-200" : "bg-slate-200" )} > - {type === "LXC" ? ( + {type === "lxc" ? ( <Icon name="package" size={22} /> ) : ( <Icon name="monitor" size={22} /> @@ -72,24 +72,28 @@ export default function NodePage() { </View> )} </View> - {node.rdd.isSuccess && ( + {node.rddData.isSuccess && ( <View> <View style={tw.style("mb-2")}> - <ProgressBar label="CPU" value={node.rdd.data.cpu} max={1} /> + <ProgressBar + label="CPU" + value={node.rddData.data.cpu} + max={1} + /> </View> <View style={tw.style("mb-2")}> <ProgressBar label="Memory" - value={node.rdd.data.memused} - max={node.rdd.data.memtotal} + value={node.rddData.data.memused} + max={node.rddData.data.memtotal} formatFn={formatBytes} /> </View> <View style={tw.style("mb-2")}> <ProgressBar label="Storage" - value={node.rdd.data.rootused} - max={node.rdd.data.roottotal} + value={node.rddData.data.rootused} + max={node.rddData.data.roottotal} formatFn={formatBytes} /> </View> @@ -101,13 +105,13 @@ export default function NodePage() { <View style={tw.style("flex-1")}> <Gauge label="Up" - value={`${formatBytes(node.rdd.data.netout)}/s`} + value={`${formatBytes(node.rddData.data.netout)}/s`} /> </View> <View style={tw.style("flex-1")}> <Gauge label="Down" - value={`${formatBytes(node.rdd.data.netin)}/s`} + value={`${formatBytes(node.rddData.data.netin)}/s`} /> </View> </View> @@ -120,12 +124,12 @@ export default function NodePage() { <Link key={lxc.vmid} href={{ - pathname: "/nodes/[name]/lxc/[vmid]", - params: { name: nodeName, vmid: lxc.vmid }, + pathname: "/nodes/[node]/lxc/[vmid]", + params: { node: nodeName, vmid: lxc.vmid }, }} style={tw.style("m-2")} > - <ResourceListItem type="LXC" resource={lxc} /> + <ResourceListItem type="lxc" resource={lxc} /> </Link> ))} </Card> @@ -134,12 +138,12 @@ export default function NodePage() { <Link key={vm.vmid} href={{ - pathname: "/nodes/[node]/lxc/[vmid]", + pathname: "/nodes/[node]/qemu/[vmid]", params: { node: nodeName, vmid: vm.vmid }, }} style={tw.style("m-2")} > - <ResourceListItem type="QEMU" resource={vm} /> + <ResourceListItem type="qemu" resource={vm} /> </Link> ))} </Card> diff --git a/app/nodes/[node]/lxc/[vmid]/console.tsx b/app/nodes/[node]/lxc/[vmid]/console.tsx index e01b898..c12eed4 100644 --- a/app/nodes/[node]/lxc/[vmid]/console.tsx +++ b/app/nodes/[node]/lxc/[vmid]/console.tsx @@ -14,21 +14,20 @@ function buildConsoleUrl(domain: string, node: string, vmid: string) { } export default function QEMUResourceConsolePage() { - const { name, vmid } = useSearchParams<{ name: string; vmid: string }>(); + const { node, vmid } = useSearchParams<{ node: string; vmid: string }>(); const { domain, ticketData } = useAuthStore(); + console.log({ ticketData }, buildConsoleUrl(domain, node, vmid)); return ( <SafeAreaView style={{ flex: 1 }}> <WebView source={{ - uri: buildConsoleUrl(domain, name, vmid), + uri: buildConsoleUrl(domain, node, vmid), headers: { Cookie: `PVEAuthCookie=${ticketData.data.ticket}`, - CSRFPreventionToken: ticketData.data.CSRFPreventionToken, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", }, }} - allowsFullscreenVideo={true} scalesPageToFit={false} injectedJavaScript={` const meta = document.createElement('meta'); diff --git a/app/nodes/[node]/lxc/[vmid]/index.tsx b/app/nodes/[node]/lxc/[vmid]/index.tsx index 6db7e03..9e8fb5d 100644 --- a/app/nodes/[node]/lxc/[vmid]/index.tsx +++ b/app/nodes/[node]/lxc/[vmid]/index.tsx @@ -1,23 +1,128 @@ -import { View, Text } from "react-native"; +import { useRouter, useSearchParams } from "expo-router"; +import { useMemo } from "react"; +import { View, Text, TouchableHighlight, ScrollView } from "react-native"; +import Icon from "@expo/vector-icons/Feather"; import tw from "twrnc"; +import Card from "../../../../../components/Card"; +import { Gauge } from "../../../../../components/Gauge"; +import ProgressBar from "../../../../../components/ProgressBar"; +import { useNode } from "../../../../../hooks/useNode"; +import { useResource } from "../../../../../hooks/useResource"; +import { formatBytes } from "../../../../../lib/helper/format"; export default function LXCResourcePage() { - return ( - <View> - <View - style={tw.style("bg-white m-2 p-3 rounded-lg border border-slate-200")} - > - <Text>Currently not supported</Text> - </View> + const router = useRouter(); + const { node, vmid } = useSearchParams<{ node: string; vmid: string }>(); + const { rddData, config } = useResource(node, "lxc", vmid); + const { lxc: lxcs } = useNode(node); - <Text style={tw.style("ml-6 mt-4")}>Config</Text> - <View - style={tw.style("bg-white m-2 p-1 rounded-lg border border-slate-200")} + const status = useMemo(() => { + if (lxcs.isSuccess) { + return lxcs.data.find((lxc) => lxc.vmid.toString() === vmid); + } + }, [lxcs]); + + return ( + <ScrollView> + <Card> + <View style={tw.style("p-1")}> + <View style={tw.style("px-1 mb-4")}> + <View style={tw.style("flex flex-row items-center")}> + <View + style={tw.style( + "h-12 w-12 rounded-lg flex flex-row justify-center items-center", + status.status === "running" ? "bg-green-200" : "bg-slate-200" + )} + > + <Icon name="monitor" size={22} /> + </View> + <View style={tw.style("mx-2 flex-1")}> + <Text style={tw.style("text-xl")}> + {vmid}: {status.name} + </Text> + <Text style={tw.style("text-base")}>Node: {node}</Text> + </View> + <View + style={tw.style( + "h-12 w-12 flex flex-row justify-center items-center" + )} + > + <Icon name="power" size={22} /> + </View> + </View> + </View> + {rddData.isSuccess && ( + <View> + <View style={tw.style("mb-2")}> + <ProgressBar label="CPU" value={rddData.data.cpu} max={1} /> + </View> + <View style={tw.style("mb-2")}> + <ProgressBar + label="Memory" + value={rddData.data.mem ?? 1} + max={rddData.data.maxmem ?? 1} + formatFn={formatBytes} + /> + </View> + <View style={tw.style("mb-2")}> + <ProgressBar + label="Storage" + value={rddData.data.disk ?? 1} + max={rddData.data.maxdisk ?? 1} + formatFn={formatBytes} + /> + </View> + <View + style={tw.style( + "flex flex-row justify-evenly items-center p-2" + )} + > + <View style={tw.style("flex-1")}> + <Gauge + label="Up" + value={`${formatBytes(rddData.data.netout)}/s`} + /> + </View> + <View style={tw.style("flex-1")}> + <Gauge + label="Down" + value={`${formatBytes(rddData.data.netin)}/s`} + /> + </View> + </View> + </View> + )} + </View> + </Card> + + <TouchableHighlight + style={tw.style("m-2 rounded-lg bg-slate-800")} + onPress={() => + router.push({ + pathname: "/nodes/[node]/lxc/[vmid]/console", + params: { node, vmid }, + }) + } > - <Text style={tw.style("px-2 py-7 flex flex-row text-center")}> - Currently not supported - </Text> - </View> - </View> + <View style={tw.style("flex flex-row m-4 items-center justify-center")}> + <Icon name="terminal" size={22} style={tw.style("mr-2 text-white")} /> + <Text style={tw.style("text-white")}>Open terminal</Text> + </View> + </TouchableHighlight> + + <Card> + <View style={tw.style("m-2")}> + {config.isSuccess && + Object.entries(config.data).map(([key, val]) => { + return ( + <View key={key} style={tw.style("mb-1")}> + <Text style={tw.style("text-lg")}>{key}:</Text> + <Text>{val}</Text> + </View> + ); + })} + </View> + </Card> + </ScrollView> ); } diff --git a/app/nodes/index.tsx b/app/nodes/index.tsx index 1cc298b..a83ce75 100644 --- a/app/nodes/index.tsx +++ b/app/nodes/index.tsx @@ -1,5 +1,5 @@ import { Link } from "expo-router"; -import { View, Text, SafeAreaView } from "react-native"; +import { View, Text } from "react-native"; import { ProxmoxNode, useNodes } from "../../hooks/useNodes"; import Icon from "@expo/vector-icons/Feather"; import tw from "twrnc"; diff --git a/components/Card/index.tsx b/components/Card/index.tsx index d958b40..900f61b 100644 --- a/components/Card/index.tsx +++ b/components/Card/index.tsx @@ -9,7 +9,7 @@ interface CardProps { export default function Card({ label, children }: CardProps) { return ( <> - {label && <Text style={tw.style("ml-6 mt-4")}>{label}</Text>} + {label && <Text style={tw.style("ml-6 mt-2")}>{label}</Text>} <View style={tw.style("bg-white m-2 p-1 rounded-lg border border-slate-200")} > diff --git a/components/Login/index.tsx b/components/Login/index.tsx index da8242d..bb5bab1 100644 --- a/components/Login/index.tsx +++ b/components/Login/index.tsx @@ -1,10 +1,10 @@ import { View, - TouchableOpacity, Text, TextInput, SafeAreaView, TextInputProps, + TouchableHighlight, } from "react-native"; import React, { useState } from "react"; import tw from "twrnc"; @@ -92,14 +92,14 @@ export default function Login() { secureTextEntry /> </View> - <TouchableOpacity + <TouchableHighlight style={tw.style( "flex bg-slate-700 rounded-md flex flex-row items-center justify-center" )} onPress={() => ticketMut.mutate({ domain, username, password })} > <Text style={tw.style("text-white font-semibold py-3")}>Sign In</Text> - </TouchableOpacity> + </TouchableHighlight> </SafeAreaView> </View> ); diff --git a/hooks/useNode.ts b/hooks/useNode.ts index bc6c349..28dd30d 100644 --- a/hooks/useNode.ts +++ b/hooks/useNode.ts @@ -1,7 +1,7 @@ import { useQueries, useQuery } from "react-query"; import useAuthStore from "../stores/useAuthStore"; -export type ResourceType = "LXC" | "QEMU"; +export type ResourceType = "lxc" | "qemu"; export interface NodeResource { status: "stopped" | "running"; @@ -18,7 +18,7 @@ export interface NodeStatus { kversion: string; } -export interface NodeRDD { +export interface NodeRddData { netin: number; netout: number; rootused: number; @@ -30,17 +30,17 @@ export interface NodeRDD { export function useNode(name: string) { const http = useAuthStore((state) => state.http); - const [rdd, status, lxc, qemu] = useQueries([ + const [rddData, status, lxc, qemu] = useQueries([ { - queryKey: ["nodes", name, "rdd"], + queryKey: ["nodes", name, "rddData"], queryFn: () => - http.get<{ data: NodeRDD[] }>(`/api2/json/nodes/${name}/rrddata`, { + http.get<{ data: NodeRddData[] }>(`/api2/json/nodes/${name}/rrddata`, { params: { timeframe: "hour", }, }), enabled: !!name, - select: (data): NodeRDD => data.data.data.at(-1), + select: (data): NodeRddData => data.data.data.at(-1), }, { queryKey: ["nodes", name, "status"], @@ -66,7 +66,7 @@ export function useNode(name: string) { ]); return { - rdd, + rddData, status, lxc, qemu, diff --git a/hooks/useNodes.ts b/hooks/useNodes.ts index 81b7da6..444eee8 100644 --- a/hooks/useNodes.ts +++ b/hooks/useNodes.ts @@ -21,22 +21,6 @@ export function useNodes() { ["nodes"], async () => { return http.get<GetNodeResp>("/api2/json/nodes"); - // return { - // data: { - // data: [ - // { - // cpu: 0.0166442953020134, - // mem: 21713018880, - // maxmem: 29306216448, - // uptime: 4854322, - // status: "online", - // maxcpu: 12, - // node: "pve", - // }, - // ] as ProxmoxNode[], - // }, - // isSuccess: true, - // }; }, { select: (data) => data.data.data, refetchInterval: 6000 } ); diff --git a/hooks/useResource.ts b/hooks/useResource.ts new file mode 100644 index 0000000..a6dc659 --- /dev/null +++ b/hooks/useResource.ts @@ -0,0 +1,52 @@ +import { useQueries } from "react-query"; +import useAuthStore from "../stores/useAuthStore"; +import { ResourceType } from "./useNode"; + +interface ResourceRddData { + cpu: number; + disk: number; + maxdisk: number; + mem: number; + maxmem: number; + netin: number; + netout: number; +} + +export function useResource( + node: string, + type: ResourceType, + vmid: string | number +) { + console.log({ node, type, vmid }); + const http = useAuthStore((state) => state.http); + const [rddData, config] = useQueries([ + { + queryKey: ["nodes", node, type, vmid, "rdd"], + queryFn: () => + http.get<{ data: ResourceRddData[] }>( + `/api2/json/nodes/${node}/${type}/${vmid}/rrddata`, + { + params: { + timeframe: "hour", + }, + } + ), + enabled: !!(node && vmid), + select: (data): ResourceRddData => data.data.data.at(-1), + }, + { + queryKey: ["nodes", node, type, vmid, "config"], + queryFn: () => + http.get<{ data: object }>( + `/api2/json/nodes/${node}/${type}/${vmid}/config` + ), + enabled: !!(node && vmid), + select: (data): object => data.data.data, + }, + ]); + + return { + rddData, + config, + }; +} diff --git a/hooks/useTicket.ts b/hooks/useTicket.ts index 0c15e33..f6253a8 100644 --- a/hooks/useTicket.ts +++ b/hooks/useTicket.ts @@ -19,7 +19,7 @@ export async function createTicket({ username, password, }: CreateTicketOpts) { - const url = `${domain}/api2/json/access/ticket`; + const url = new URL("/api2/json/access/ticket", domain).toString(); const headers = { "Content-Type": "application/json" }; return axios.post<CreateTicketResp>(url, { username, password }, { headers }); }