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>
|
Use [`expo-router`](https://expo.github.io/router) to build native navigation using files in the `app/` directory.
|
||||||
<!-- 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.
|
|
||||||
|
|
||||||
## 🚀 How to use
|
## 🚀 How to use
|
||||||
|
|
||||||
#### Creating a new project
|
```sh
|
||||||
|
npx create-react-native-app -t with-router
|
||||||
- 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`
|
|
||||||
|
|
||||||
## 📝 Notes
|
## 📝 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": {
|
"expo": {
|
||||||
|
"scheme": "pvern",
|
||||||
|
"web": {
|
||||||
|
"bundler": "metro"
|
||||||
|
},
|
||||||
"name": "pvern",
|
"name": "pvern",
|
||||||
"slug": "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) {
|
module.exports = function (api) {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo'],
|
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": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo start --android",
|
"android": "expo start --android",
|
||||||
"ios": "expo start --ios",
|
"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"
|
"extends": "expo/tsconfig.base"
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user