diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a943401 --- /dev/null +++ b/.vscode/settings.json @@ -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\\(([^)]*)\\)" + ] +} diff --git a/App.tsx b/App.tsx deleted file mode 100644 index a6e41fe..0000000 --- a/App.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { StyleSheet, Text, View } from 'react-native'; - -export default function App() { - return ( - - Open up App.tsx to start working on your app! - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/README.md b/README.md index 1987692..ea0f4db 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,15 @@ -# TypeScript Example +# Expo Router Example -

- - Supports Expo iOS - - Supports Expo Android - - Supports Expo Web -

- -```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) diff --git a/app.json b/app.json index b6e09dc..090ba23 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,10 @@ { "expo": { + "scheme": "pvern", + "web": { + "bundler": "metro" + }, "name": "pvern", "slug": "pvern" } -} +} \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx new file mode 100644 index 0000000..74b2b38 --- /dev/null +++ b/app/_layout.tsx @@ -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 ; +} + +export default function Layout() { + const { isActive } = useAuthStore(); + + return ( + + {isActive ? ( + + ) : ( + + )} + + ); +} diff --git a/app/index.tsx b/app/index.tsx new file mode 100644 index 0000000..e47a4c2 --- /dev/null +++ b/app/index.tsx @@ -0,0 +1,5 @@ +import { Redirect } from "expo-router"; + +export default function RootPage() { + return ; +} diff --git a/app/nodes/[node]/index.tsx b/app/nodes/[node]/index.tsx new file mode 100644 index 0000000..b4d2953 --- /dev/null +++ b/app/nodes/[node]/index.tsx @@ -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 ( + + + {type === "LXC" ? ( + + ) : ( + + )} + + + + {resource.vmid}: {resource.name} + + + CPUs: {resource.cpus} + + MEM: {formatBytes(resource.maxmem)} + + DISK: {formatBytes(resource.maxdisk)} + + + + ); +} + +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 ( + + + + {label} ({percentage}) + + {formatFn && ( + + {formatFn(value)}/{formatFn(max)} + + )} + + + + + + ); +} + +interface GaugeProps { + label: string; + value: string; +} + +export function Gauge({ label, value }: GaugeProps) { + return ( + + {label} + {value} + + ); +} + +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 ( + + + + + {nodeName} + {node.status.isSuccess && ( + + {node.status.data.pveversion} + {node.status.data.kversion} + + )} + {node.rdd.isSuccess && ( + + + + + + + + + + + + + + + + + + + + )} + + LXC Containers + + {sortedLXCs.map((lxc) => ( + + + + ))} + + Virtual Machines + + {sortedVMs.map((vm) => ( + + + + ))} + + + + + ); +} diff --git a/app/nodes/[node]/lxc/[vmid]/console.tsx b/app/nodes/[node]/lxc/[vmid]/console.tsx new file mode 100644 index 0000000..e01b898 --- /dev/null +++ b/app/nodes/[node]/lxc/[vmid]/console.tsx @@ -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 ( + + + + ); +} diff --git a/app/nodes/[node]/lxc/[vmid]/index.tsx b/app/nodes/[node]/lxc/[vmid]/index.tsx new file mode 100644 index 0000000..6db7e03 --- /dev/null +++ b/app/nodes/[node]/lxc/[vmid]/index.tsx @@ -0,0 +1,23 @@ +import { View, Text } from "react-native"; +import tw from "twrnc"; + +export default function LXCResourcePage() { + return ( + + + Currently not supported + + + Config + + + Currently not supported + + + + ); +} diff --git a/app/nodes/index.tsx b/app/nodes/index.tsx new file mode 100644 index 0000000..a2cdb79 --- /dev/null +++ b/app/nodes/index.tsx @@ -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 ( + + + {node.status !== "unknown" ? ( + + ) : ( + ? + )} + + + {node.node} + + + CPU: {formatPercentage(node.cpu)} + + MEM: {formatPercentage(node.mem / node.maxmem)} + + + + ); +} + +export default function HomePage() { + const nodes = useNodes(); + + return ( + + + + Nodes + + {nodes.isSuccess && + nodes.data.map((node) => ( + + + + ))} + + + Storage + + + Currently not supported + + + + + + ); +} diff --git a/babel.config.js b/babel.config.js index 2900afe..355556e 100644 --- a/babel.config.js +++ b/babel.config.js @@ -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"), + ], + }; }; diff --git a/components/Login/index.tsx b/components/Login/index.tsx new file mode 100644 index 0000000..5ab9524 --- /dev/null +++ b/components/Login/index.tsx @@ -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 ( + + Hello + + + +