Initial commit
This commit is contained in:
parent
25d5321778
commit
e54129908d
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
13
.vscode/settings.json
vendored
Normal file
13
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
"tw`([^`]*)",
|
||||
"tw\\.style\\(([^)]*)\\)"
|
||||
]
|
||||
}
|
18
App.tsx
18
App.tsx
@ -1,18 +0,0 @@
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Open up App.tsx to start working on your app!</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
37
README.md
37
README.md
@ -1,36 +1,15 @@
|
||||
# TypeScript Example
|
||||
# Expo Router Example
|
||||
|
||||
<p>
|
||||
<!-- iOS -->
|
||||
<img alt="Supports Expo iOS" longdesc="Supports Expo iOS" src="https://img.shields.io/badge/iOS-4630EB.svg?style=flat-square&logo=APPLE&labelColor=999999&logoColor=fff" />
|
||||
<!-- Android -->
|
||||
<img alt="Supports Expo Android" longdesc="Supports Expo Android" src="https://img.shields.io/badge/Android-4630EB.svg?style=flat-square&logo=ANDROID&labelColor=A4C639&logoColor=fff" />
|
||||
<!-- Web -->
|
||||
<img alt="Supports Expo Web" longdesc="Supports Expo Web" src="https://img.shields.io/badge/web-4630EB.svg?style=flat-square&logo=GOOGLE-CHROME&labelColor=4285F4&logoColor=fff" />
|
||||
</p>
|
||||
|
||||
```sh
|
||||
npx create-react-native-app -t with-typescript
|
||||
```
|
||||
|
||||
TypeScript is a superset of JavaScript which gives you static types and powerful tooling in Visual Studio Code including autocompletion and useful inline warnings for type errors.
|
||||
Use [`expo-router`](https://expo.github.io/router) to build native navigation using files in the `app/` directory.
|
||||
|
||||
## 🚀 How to use
|
||||
|
||||
#### Creating a new project
|
||||
|
||||
- Install the CLI: `npm i -g expo-cli`
|
||||
- Create a project: `npx create-react-native-app -t with-typescript`
|
||||
- `cd` into the project
|
||||
|
||||
### Adding TypeScript to existing projects
|
||||
|
||||
- Create a blank TypeScript config: `touch tsconfig.json`
|
||||
- Run `yarn start` or `npm run start` to automatically configure TypeScript
|
||||
- Rename files to TypeScript, `.tsx` for React components and `.ts` for plain typescript files
|
||||
|
||||
> 💡 You can disable the TypeScript setup in Expo CLI with the environment variable `EXPO_NO_TYPESCRIPT_SETUP=1 expo start`
|
||||
```sh
|
||||
npx create-react-native-app -t with-router
|
||||
```
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- [Expo TypeScript guide](https://docs.expo.dev/versions/latest/guides/typescript/)
|
||||
- [Expo Router: Docs](https://expo.github.io/router)
|
||||
- [Expo Router: Repo](https://github.com/expo/router)
|
||||
- [Request for Comments](https://github.com/expo/router/discussions/1)
|
||||
|
6
app.json
6
app.json
@ -1,6 +1,10 @@
|
||||
{
|
||||
"expo": {
|
||||
"scheme": "pvern",
|
||||
"web": {
|
||||
"bundler": "metro"
|
||||
},
|
||||
"name": "pvern",
|
||||
"slug": "pvern"
|
||||
}
|
||||
}
|
||||
}
|
29
app/_layout.tsx
Normal file
29
app/_layout.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import Auth from "../components/Login";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import Icon from "@expo/vector-icons/Feather";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function LogoutButton() {
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
return <Icon name="log-out" size={22} onPress={logout} />;
|
||||
}
|
||||
|
||||
export default function Layout() {
|
||||
const { isActive } = useAuthStore();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{isActive ? (
|
||||
<Stack
|
||||
screenOptions={{ headerRight: LogoutButton, headerTitle: "PVERN" }}
|
||||
initialRouteName="/test"
|
||||
/>
|
||||
) : (
|
||||
<Auth />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
5
app/index.tsx
Normal file
5
app/index.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { Redirect } from "expo-router";
|
||||
|
||||
export default function RootPage() {
|
||||
return <Redirect href="/nodes" />;
|
||||
}
|
207
app/nodes/[node]/index.tsx
Normal file
207
app/nodes/[node]/index.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import { Link, Stack, useSearchParams } from "expo-router";
|
||||
import { View, Text, SafeAreaView, ScrollView } from "react-native";
|
||||
import { NodeResource, 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";
|
||||
|
||||
interface ResourceListItemProps {
|
||||
type: "LXC" | "QEMU";
|
||||
resource: NodeResource;
|
||||
}
|
||||
|
||||
export function ResourceListItem({ type, resource }: ResourceListItemProps) {
|
||||
return (
|
||||
<View style={tw.style("flex-row items-center")}>
|
||||
<View
|
||||
style={tw.style(
|
||||
"h-12 w-12 rounded-lg flex flex-row justify-center items-center",
|
||||
resource.status === "running" ? "bg-green-200" : "bg-slate-200"
|
||||
)}
|
||||
>
|
||||
{type === "LXC" ? (
|
||||
<Icon name="package" size={22} />
|
||||
) : (
|
||||
<Icon name="monitor" size={22} />
|
||||
)}
|
||||
</View>
|
||||
<View style={tw.style("ml-2")}>
|
||||
<Text style={tw.style("text-xl")}>
|
||||
{resource.vmid}: {resource.name}
|
||||
</Text>
|
||||
<View style={tw.style("flex flex-row")}>
|
||||
<Text style={tw.style("pr-2")}>CPUs: {resource.cpus}</Text>
|
||||
<Text style={tw.style("pr-2")}>
|
||||
MEM: {formatBytes(resource.maxmem)}
|
||||
</Text>
|
||||
<Text>DISK: {formatBytes(resource.maxdisk)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProgressBarProps {
|
||||
label: string;
|
||||
value: number;
|
||||
max: number;
|
||||
formatFn?: (input: number) => string;
|
||||
}
|
||||
|
||||
export function ProgressBar({ label, value, max, formatFn }: ProgressBarProps) {
|
||||
const percentage = formatPercentage(value / max);
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={tw.style("flex flex-row justify-between items-center mb-1 px-1")}
|
||||
>
|
||||
<Text>
|
||||
{label} ({percentage})
|
||||
</Text>
|
||||
{formatFn && (
|
||||
<Text>
|
||||
{formatFn(value)}/{formatFn(max)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={tw.style("h-3 w-full bg-slate-300 rounded-lg")}>
|
||||
<View
|
||||
style={tw.style("h-full bg-slate-700 rounded-lg", {
|
||||
width: percentage,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface GaugeProps {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function Gauge({ label, value }: GaugeProps) {
|
||||
return (
|
||||
<View>
|
||||
<Text>{label}</Text>
|
||||
<Text style={tw.style("text-2xl")}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NodePage() {
|
||||
const { node: nodeName } = useSearchParams<{ node: string }>();
|
||||
const node = useNode(nodeName);
|
||||
|
||||
const sortedLXCs = useMemo(() => {
|
||||
if (!node.lxc.isSuccess) return [];
|
||||
return node.lxc.data.sort((a, b) => a.vmid - b.vmid);
|
||||
}, [node.lxc.data]);
|
||||
|
||||
const sortedVMs = useMemo(() => {
|
||||
if (!node.qemu.isSuccess) return [];
|
||||
return node.qemu.data.sort((a, b) => a.vmid - b.vmid);
|
||||
}, [node.qemu.data]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<SafeAreaView>
|
||||
<ScrollView>
|
||||
<View
|
||||
style={tw.style(
|
||||
"bg-white m-2 p-3 rounded-lg border border-slate-200"
|
||||
)}
|
||||
>
|
||||
<Text style={tw.style("text-2xl font-semibold")}>{nodeName}</Text>
|
||||
{node.status.isSuccess && (
|
||||
<View style={tw.style("mb-4")}>
|
||||
<Text>{node.status.data.pveversion}</Text>
|
||||
<Text>{node.status.data.kversion}</Text>
|
||||
</View>
|
||||
)}
|
||||
{node.rdd.isSuccess && (
|
||||
<View>
|
||||
<View style={tw.style("mb-2")}>
|
||||
<ProgressBar label="CPU" value={node.rdd.data.cpu} max={1} />
|
||||
</View>
|
||||
<View style={tw.style("mb-2")}>
|
||||
<ProgressBar
|
||||
label="Memory"
|
||||
value={node.rdd.data.memused}
|
||||
max={node.rdd.data.memtotal}
|
||||
formatFn={formatBytes}
|
||||
/>
|
||||
</View>
|
||||
<View style={tw.style("mb-2")}>
|
||||
<ProgressBar
|
||||
label="Storage"
|
||||
value={node.rdd.data.rootused}
|
||||
max={node.rdd.data.roottotal}
|
||||
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(node.rdd.data.netout)}/s`}
|
||||
/>
|
||||
</View>
|
||||
<View style={tw.style("flex-1")}>
|
||||
<Gauge
|
||||
label="Down"
|
||||
value={`${formatBytes(node.rdd.data.netin)}/s`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={tw.style("ml-6 mt-4")}>LXC Containers</Text>
|
||||
<View
|
||||
style={tw.style(
|
||||
"bg-white m-2 p-1 rounded-lg border border-slate-200"
|
||||
)}
|
||||
>
|
||||
{sortedLXCs.map((lxc) => (
|
||||
<Link
|
||||
key={lxc.vmid}
|
||||
href={{
|
||||
pathname: "/nodes/[name]/lxc/[vmid]",
|
||||
params: { name: nodeName, vmid: lxc.vmid },
|
||||
}}
|
||||
style={tw.style("m-2")}
|
||||
>
|
||||
<ResourceListItem type="LXC" resource={lxc} />
|
||||
</Link>
|
||||
))}
|
||||
</View>
|
||||
<Text style={tw.style("ml-6 mt-4")}>Virtual Machines</Text>
|
||||
<View
|
||||
style={tw.style(
|
||||
"bg-white m-2 p-1 rounded-lg border border-slate-200"
|
||||
)}
|
||||
>
|
||||
{sortedVMs.map((vm) => (
|
||||
<Link
|
||||
key={vm.vmid}
|
||||
href={{
|
||||
pathname: "/nodes/[node]/lxc/[vmid]",
|
||||
params: { node: nodeName, vmid: vm.vmid },
|
||||
}}
|
||||
style={tw.style("m-2")}
|
||||
>
|
||||
<ResourceListItem type="QEMU" resource={vm} />
|
||||
</Link>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
42
app/nodes/[node]/lxc/[vmid]/console.tsx
Normal file
42
app/nodes/[node]/lxc/[vmid]/console.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { useSearchParams } from "expo-router";
|
||||
import { SafeAreaView } from "react-native";
|
||||
import { WebView } from "react-native-webview";
|
||||
import useAuthStore from "../../../../../stores/useAuthStore";
|
||||
|
||||
function buildConsoleUrl(domain: string, node: string, vmid: string) {
|
||||
const url = new URL(domain);
|
||||
url.searchParams.append("node", node);
|
||||
url.searchParams.append("vmid", vmid);
|
||||
url.searchParams.append("resize", "1");
|
||||
url.searchParams.append("console", "lxc");
|
||||
url.searchParams.append("xtermjs", "1");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export default function QEMUResourceConsolePage() {
|
||||
const { name, vmid } = useSearchParams<{ name: string; vmid: string }>();
|
||||
const { domain, ticketData } = useAuthStore();
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<WebView
|
||||
source={{
|
||||
uri: buildConsoleUrl(domain, name, 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');
|
||||
meta.setAttribute('content', 'width=width, initial-scale=0.5, maximum-scale=0.5, user-scalable=2.0');
|
||||
meta.setAttribute('name', 'viewport');
|
||||
document.getElementsByTagName('head')[0].appendChild(meta);
|
||||
`}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
23
app/nodes/[node]/lxc/[vmid]/index.tsx
Normal file
23
app/nodes/[node]/lxc/[vmid]/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { View, Text } from "react-native";
|
||||
import tw from "twrnc";
|
||||
|
||||
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>
|
||||
|
||||
<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")}
|
||||
>
|
||||
<Text style={tw.style("px-2 py-7 flex flex-row text-center")}>
|
||||
Currently not supported
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
80
app/nodes/index.tsx
Normal file
80
app/nodes/index.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { Link } from "expo-router";
|
||||
import { View, Text, SafeAreaView } from "react-native";
|
||||
import { ProxmoxNode, useNodes } from "../../hooks/useNodes";
|
||||
import Icon from "@expo/vector-icons/Feather";
|
||||
import tw from "twrnc";
|
||||
import { formatPercentage } from "../../lib/helper/format";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
|
||||
export function NodeListItem({ node }: { node: ProxmoxNode }) {
|
||||
return (
|
||||
<View style={tw.style("flex-row items-center")}>
|
||||
<View
|
||||
style={tw.style(
|
||||
"h-12 w-12 rounded-lg flex flex-row justify-center items-center",
|
||||
node.status === "online" ? "bg-green-200" : "bg-slate-200"
|
||||
)}
|
||||
>
|
||||
{node.status !== "unknown" ? (
|
||||
<Icon name="server" size={22} />
|
||||
) : (
|
||||
<Text style={tw.style("text-xl")}>?</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={tw.style("ml-2")}>
|
||||
<Text style={tw.style("text-xl")}>{node.node}</Text>
|
||||
<View style={tw.style("flex flex-row")}>
|
||||
<Text style={tw.style("pr-2")}>
|
||||
CPU: {formatPercentage(node.cpu)}
|
||||
</Text>
|
||||
<Text>MEM: {formatPercentage(node.mem / node.maxmem)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const nodes = useNodes();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ScrollView>
|
||||
<SafeAreaView>
|
||||
<Text style={tw.style("ml-6 mt-4")}>Nodes</Text>
|
||||
<View
|
||||
style={tw.style(
|
||||
"bg-white m-2 p-1 rounded-lg border border-slate-200"
|
||||
)}
|
||||
>
|
||||
{nodes.isSuccess &&
|
||||
nodes.data.map((node) => (
|
||||
<Link
|
||||
key={node.node}
|
||||
href={{
|
||||
pathname: "/nodes/[node]",
|
||||
params: { node: node.node },
|
||||
}}
|
||||
disabled={node.status !== "online"}
|
||||
style={tw.style("p-2")}
|
||||
>
|
||||
<NodeListItem node={node} />
|
||||
</Link>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={tw.style("ml-6 mt-4")}>Storage</Text>
|
||||
<View
|
||||
style={tw.style(
|
||||
"bg-white m-2 p-1 rounded-lg border border-slate-200"
|
||||
)}
|
||||
>
|
||||
<Text style={tw.style("px-2 py-7 flex flex-row text-center")}>
|
||||
Currently not supported
|
||||
</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
@ -1,6 +1,11 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ["babel-preset-expo"],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-export-namespace-from",
|
||||
"react-native-reanimated/plugin",
|
||||
require.resolve("expo-router/babel"),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
39
components/Login/index.tsx
Normal file
39
components/Login/index.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { View, Button, Text, TextInput } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import useAuthStore from "../../stores/useAuthStore";
|
||||
import { useTicketMut } from "../../hooks/useTicket";
|
||||
|
||||
export default function Login() {
|
||||
const [domain, setDomain] = useState("");
|
||||
const [username, setUsername] = useState("root@pam");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const ticketMut = useTicketMut({
|
||||
onSuccess: ({ data: ticketData }) => {
|
||||
console.log({ ticketData });
|
||||
authStore.update({ domain, username, ticketData });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>Hello</Text>
|
||||
<TextInput value={domain} onChangeText={setDomain} placeholder="Domain" />
|
||||
<TextInput
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
placeholder="Username"
|
||||
/>
|
||||
<TextInput
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<Button
|
||||
title="Login"
|
||||
onPress={() => ticketMut.mutate({ domain, username, password })}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
130
hooks/useNode.ts
Normal file
130
hooks/useNode.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { useQueries, useQuery } from "react-query";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
|
||||
export type ResourceType = "LXC" | "QEMU";
|
||||
|
||||
export interface NodeResource {
|
||||
status: "stopped" | "running";
|
||||
vmid: number;
|
||||
name: string;
|
||||
tags: string;
|
||||
cpus: number;
|
||||
maxdisk: number;
|
||||
maxmem: number;
|
||||
}
|
||||
|
||||
export interface NodeStatus {
|
||||
pveversion: string;
|
||||
kversion: string;
|
||||
}
|
||||
|
||||
export interface NodeRDD {
|
||||
netin: number;
|
||||
netout: number;
|
||||
rootused: number;
|
||||
roottotal: number;
|
||||
memused: number;
|
||||
memtotal: number;
|
||||
cpu: number;
|
||||
}
|
||||
|
||||
export function useNode(name: string) {
|
||||
// const http = useAuthStore((state) => state.http);
|
||||
// const [rdd, status, lxc, qemu] = useQueries([
|
||||
// {
|
||||
// queryKey: ["nodes", name, "rdd"],
|
||||
// queryFn: () =>
|
||||
// http.get<{ data: NodeRDD[] }>(`/api2/json/nodes/${name}/rrddata`, {
|
||||
// params: {
|
||||
// timeframe: "hour",
|
||||
// },
|
||||
// }),
|
||||
// enabled: !!name,
|
||||
// select: (data): NodeRDD => data.data.data.at(-1),
|
||||
// },
|
||||
// {
|
||||
// queryKey: ["nodes", name, "status"],
|
||||
// queryFn: () =>
|
||||
// http.get<{ data: NodeStatus }>(`/api2/json/nodes/${name}/status`),
|
||||
// enabled: !!name,
|
||||
// select: (data): NodeStatus => data.data.data,
|
||||
// },
|
||||
// {
|
||||
// queryKey: ["nodes", name, "lxc"],
|
||||
// queryFn: () =>
|
||||
// http.get<{ data: NodeResource[] }>(`/api2/json/nodes/${name}/lxc`),
|
||||
// enabled: !!name,
|
||||
// select: (data): NodeResource[] => data.data.data,
|
||||
// },
|
||||
// {
|
||||
// queryKey: ["nodes", name, "qemu"],
|
||||
// queryFn: () =>
|
||||
// http.get<{ data: NodeResource[] }>(`/api2/json/nodes/${name}/qemu`),
|
||||
// enabled: !!name,
|
||||
// select: (data): NodeResource[] => data.data.data,
|
||||
// },
|
||||
// ]);
|
||||
|
||||
// return {
|
||||
// rdd,
|
||||
// status,
|
||||
// lxc,
|
||||
// qemu,
|
||||
// };
|
||||
return {
|
||||
rdd: {
|
||||
data: {
|
||||
memused: 21691995750.4,
|
||||
roottotal: 100861726720,
|
||||
swaptotal: 8589930496,
|
||||
swapused: 315621376,
|
||||
rootused: 8778427323.73333,
|
||||
time: 1679347500,
|
||||
memtotal: 29306216448,
|
||||
iowait: 0.00668312957886097,
|
||||
netout: 41114.2883333333,
|
||||
loadavg: 0.586166666666667,
|
||||
cpu: 0.0151996855422636,
|
||||
maxcpu: 12,
|
||||
netin: 29321.46,
|
||||
},
|
||||
isSuccess: true,
|
||||
},
|
||||
lxc: {
|
||||
data: [
|
||||
{
|
||||
vmid: 101,
|
||||
cpus: 2,
|
||||
maxdisk: 8350298112,
|
||||
maxmem: 1073741824,
|
||||
name: "kibana",
|
||||
status: "running",
|
||||
tags: "",
|
||||
},
|
||||
],
|
||||
isSuccess: true,
|
||||
},
|
||||
qemu: {
|
||||
data: [
|
||||
{
|
||||
vmid: 201,
|
||||
cpus: 2,
|
||||
maxdisk: 8350298112,
|
||||
maxmem: 1073741824,
|
||||
name: "vm",
|
||||
status: "running",
|
||||
tags: "",
|
||||
},
|
||||
],
|
||||
isSuccess: true,
|
||||
},
|
||||
status: {
|
||||
data: {
|
||||
pveversion: "pve-manager/7.3-4/d69b70d4",
|
||||
kversion:
|
||||
"Linux 5.15.83-1-pve #1 SMP PVE 5.15.83-1 (2022-12-15T00:00Z)",
|
||||
},
|
||||
isSuccess: true,
|
||||
},
|
||||
};
|
||||
}
|
43
hooks/useNodes.ts
Normal file
43
hooks/useNodes.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useQuery } from "react-query";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
|
||||
export interface ProxmoxNode {
|
||||
node: string;
|
||||
status: "unknown" | "online" | "offline";
|
||||
cpu?: number;
|
||||
mem?: number;
|
||||
maxcpu?: number;
|
||||
maxmem?: number;
|
||||
uptime?: number;
|
||||
}
|
||||
|
||||
interface GetNodeResp {
|
||||
data: ProxmoxNode[];
|
||||
}
|
||||
|
||||
export function useNodes() {
|
||||
// const http = useAuthStore((state) => state.http);
|
||||
return useQuery(
|
||||
["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 }
|
||||
);
|
||||
}
|
29
hooks/useTicket.ts
Normal file
29
hooks/useTicket.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import axios from "axios";
|
||||
import { useMutation, UseMutationOptions } from "react-query";
|
||||
|
||||
interface CreateTicketOpts {
|
||||
domain: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface CreateTicketResp {
|
||||
data: {
|
||||
ticket: string;
|
||||
CSRFPreventionToken: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTicket({
|
||||
domain,
|
||||
username,
|
||||
password,
|
||||
}: CreateTicketOpts) {
|
||||
const url = `${domain}/api2/json/access/ticket`;
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
return axios.post<CreateTicketResp>(url, { username, password }, { headers });
|
||||
}
|
||||
|
||||
export function useTicketMut(options: UseMutationOptions) {
|
||||
return useMutation({ mutationFn: createTicket, ...options });
|
||||
}
|
10
lib/helper/format.ts
Normal file
10
lib/helper/format.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
export function formatPercentage(num?: number) {
|
||||
if (!num) return "N/A";
|
||||
return num.toLocaleString("en", { style: "percent" });
|
||||
}
|
||||
|
||||
export function formatBytes(num?: number) {
|
||||
return prettyBytes(num ?? 1);
|
||||
}
|
67
package.json
67
package.json
@ -1,24 +1,53 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"expo": "^47.0.0",
|
||||
"react": "18.1.0",
|
||||
"react-dom": "18.1.0",
|
||||
"react-native": "0.70.5",
|
||||
"react-native-web": "~0.18.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
"@types/react": "~18.0.24",
|
||||
"@types/react-native": "~0.70.6",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"name": "pvern",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web"
|
||||
}
|
||||
}
|
||||
"web": "expo start --web",
|
||||
"lint": "rome check .",
|
||||
"format": "rome format . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@open-draft/until": "^2.0.0",
|
||||
"@types/react": "~18.0.27",
|
||||
"axios": "^1.3.4",
|
||||
"expo": "^48.0.4",
|
||||
"expo-constants": "~14.2.1",
|
||||
"expo-linking": "~4.0.1",
|
||||
"expo-router": "^1.2.0",
|
||||
"expo-splash-screen": "~0.18.1",
|
||||
"expo-status-bar": "~1.4.2",
|
||||
"immer": "^9.0.19",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.43.7",
|
||||
"react-native": "0.71.3",
|
||||
"react-native-gesture-handler": "~2.9.0",
|
||||
"react-native-reanimated": "~2.14.4",
|
||||
"react-native-safe-area-context": "4.5.0",
|
||||
"react-native-screens": "~3.20.0",
|
||||
"react-native-web": "~0.18.7",
|
||||
"react-native-webview": "11.26.0",
|
||||
"react-query": "^3.39.3",
|
||||
"twrnc": "^3.6.0",
|
||||
"typescript": "^4.9.4",
|
||||
"zustand": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
|
||||
"rome": "^11.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"metro": "^0.73.7",
|
||||
"metro-resolver": "^0.73.7"
|
||||
},
|
||||
"overrides": {
|
||||
"metro": "^0.73.7",
|
||||
"metro-resolver": "^0.73.7"
|
||||
},
|
||||
"name": "pvern",
|
||||
"version": "1.0.0",
|
||||
"private": true
|
||||
}
|
6923
pnpm-lock.yaml
generated
6923
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
53
stores/useAuthStore.ts
Normal file
53
stores/useAuthStore.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { create } from "zustand";
|
||||
import produce from "immer";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import { CreateTicketResp } from "../hooks/useTicket";
|
||||
|
||||
interface Profile {
|
||||
username: string;
|
||||
domain: string;
|
||||
ticketData: CreateTicketResp;
|
||||
}
|
||||
|
||||
interface AuthStoreState extends Profile {
|
||||
http: AxiosInstance;
|
||||
isActive: boolean;
|
||||
logout: () => void;
|
||||
update: (state: Profile) => void;
|
||||
}
|
||||
|
||||
const useAuthStore = create<AuthStoreState>((set) => ({
|
||||
username: "",
|
||||
domain: "",
|
||||
ticketData: { data: { CSRFPreventionToken: "", ticket: "" } },
|
||||
http: axios.create(),
|
||||
isActive: false,
|
||||
logout: () =>
|
||||
set(
|
||||
produce((state: AuthStoreState) => {
|
||||
state.domain = "";
|
||||
state.username = "";
|
||||
state.http = axios.create();
|
||||
state.isActive = false;
|
||||
state.ticketData = { data: { CSRFPreventionToken: "", ticket: "" } };
|
||||
})
|
||||
),
|
||||
update: ({ domain, username, ticketData }: Profile) =>
|
||||
set(
|
||||
produce((state: AuthStoreState) => {
|
||||
state.domain = domain;
|
||||
state.username = username;
|
||||
state.ticketData = ticketData;
|
||||
state.http = axios.create({
|
||||
baseURL: domain,
|
||||
headers: {
|
||||
CSRFPreventionToken: ticketData.data.CSRFPreventionToken,
|
||||
cookie: `PVEAuthCookie=${ticketData.data.ticket}`,
|
||||
},
|
||||
});
|
||||
state.isActive = true;
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
export default useAuthStore;
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {},
|
||||
"compilerOptions": {
|
||||
"module": "ESNext"
|
||||
},
|
||||
"extends": "expo/tsconfig.base"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user