mirror of
https://github.com/khairul169/home-lab.git
synced 2025-04-28 16:49:36 +07:00
feat: project init
This commit is contained in:
parent
01e5ae1a19
commit
9402b650b6
6
app.json
6
app.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "myapp",
|
"name": "Home Lab",
|
||||||
"slug": "myapp",
|
"slug": "homelab",
|
||||||
"scheme": "myapp",
|
"scheme": "homelab",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
|
175
backend/.gitignore
vendored
Normal file
175
backend/.gitignore
vendored
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
BIN
backend/bun.lockb
Executable file
BIN
backend/bun.lockb
Executable file
Binary file not shown.
113
backend/index.ts
Normal file
113
backend/index.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { cors } from "hono/cors";
|
||||||
|
import { zValidator } from "@hono/zod-validator";
|
||||||
|
import z from "zod";
|
||||||
|
import si from "systeminformation";
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||||
|
if (bytes === 0) return "n/a";
|
||||||
|
const i = parseInt(String(Math.floor(Math.log(bytes) / Math.log(1024))), 10);
|
||||||
|
if (i === 0) return `${bytes} ${sizes[i]}`;
|
||||||
|
return `${(bytes / 1024 ** i).toFixed(2)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondsToTime = (seconds: number) => {
|
||||||
|
const d = Math.floor(seconds / (3600 * 24));
|
||||||
|
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||||
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
const s = Math.floor(seconds % 60);
|
||||||
|
return `${d}d ${h}h ${m}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
.use(cors())
|
||||||
|
|
||||||
|
.get("/", (c) => c.text("It works!"))
|
||||||
|
|
||||||
|
.get("/system", async (c) => {
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
const uptime = secondsToTime(si.time().uptime || 0);
|
||||||
|
const system = await si.system();
|
||||||
|
|
||||||
|
const cpuSpeed = await si.cpuCurrentSpeed();
|
||||||
|
const cpuTemp = await si.cpuTemperature();
|
||||||
|
const cpuLoad = await si.currentLoad();
|
||||||
|
const mem = await si.mem();
|
||||||
|
|
||||||
|
const perf = {
|
||||||
|
cpu: {
|
||||||
|
load: cpuLoad.currentLoad,
|
||||||
|
speed: cpuSpeed.avg,
|
||||||
|
temp: cpuTemp.main,
|
||||||
|
},
|
||||||
|
mem: {
|
||||||
|
total: formatBytes(mem.total),
|
||||||
|
percent: (mem.active / mem.total) * 100,
|
||||||
|
used: formatBytes(mem.active),
|
||||||
|
free: formatBytes(mem.total - mem.active),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fsMounts = await si.fsSize();
|
||||||
|
const storage = fsMounts
|
||||||
|
.filter((i) => i.size > 32 * 1024 * 1024 * 1024)
|
||||||
|
.map((i) => ({
|
||||||
|
type: i.type,
|
||||||
|
mount: i.mount,
|
||||||
|
used: formatBytes(i.used),
|
||||||
|
percent: (i.used / i.size) * 100,
|
||||||
|
total: formatBytes(i.size),
|
||||||
|
free: formatBytes(i.available),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return c.json({ uptime, date, system, perf, storage });
|
||||||
|
})
|
||||||
|
|
||||||
|
.get(
|
||||||
|
"/process",
|
||||||
|
zValidator(
|
||||||
|
"query",
|
||||||
|
z
|
||||||
|
.object({ sort: z.enum(["cpu", "mem"]), limit: z.coerce.number() })
|
||||||
|
.partial()
|
||||||
|
.optional()
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const memTotal = (await si.mem()).total;
|
||||||
|
const sort = c.req.query("sort") || "mem";
|
||||||
|
const limit = parseInt(c.req.query("limit") || "") || 10;
|
||||||
|
|
||||||
|
let processList = (await si.processes()).list;
|
||||||
|
|
||||||
|
if (sort) {
|
||||||
|
switch (sort) {
|
||||||
|
case "cpu":
|
||||||
|
processList = processList.sort((a, b) => b.cpu - a.cpu);
|
||||||
|
break;
|
||||||
|
case "mem":
|
||||||
|
processList = processList.sort((a, b) => b.mem - a.mem);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = processList
|
||||||
|
.map((p) => ({
|
||||||
|
name: p.name,
|
||||||
|
cmd: [p.name, p.params].filter(Boolean).join(" "),
|
||||||
|
cpu: p.cpu,
|
||||||
|
cpuPercent: p.cpu.toFixed(1) + "%",
|
||||||
|
mem: p.mem,
|
||||||
|
memUsage: formatBytes((p.mem / 100) * memTotal),
|
||||||
|
path: p.path,
|
||||||
|
user: p.user,
|
||||||
|
}))
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
return c.json({ list });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type AppType = typeof app;
|
||||||
|
|
||||||
|
export default app;
|
20
backend/package.json
Normal file
20
backend/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --watch index.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hono/zod-validator": "^0.2.0",
|
||||||
|
"hono": "^4.1.0",
|
||||||
|
"systeminformation": "^5.22.2",
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
}
|
||||||
|
}
|
27
backend/tsconfig.json
Normal file
27
backend/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
4
global.d.ts
vendored
Normal file
4
global.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.svg" {
|
||||||
|
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||||
|
export default content;
|
||||||
|
}
|
19
metro.config.js
Normal file
19
metro.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const { getDefaultConfig } = require("expo/metro-config");
|
||||||
|
|
||||||
|
module.exports = (() => {
|
||||||
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
|
const { transformer, resolver } = config;
|
||||||
|
|
||||||
|
config.transformer = {
|
||||||
|
...transformer,
|
||||||
|
babelTransformerPath: require.resolve("react-native-svg-transformer")
|
||||||
|
};
|
||||||
|
config.resolver = {
|
||||||
|
...resolver,
|
||||||
|
assetExts: resolver.assetExts.filter((ext) => ext !== "svg"),
|
||||||
|
sourceExts: [...resolver.sourceExts, "svg"]
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
})();
|
10
package.json
10
package.json
@ -6,22 +6,27 @@
|
|||||||
"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",
|
||||||
|
"build:web": "expo export -p web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~3.1.3",
|
"@expo/metro-runtime": "~3.1.3",
|
||||||
"@types/react": "~18.2.45",
|
"@types/react": "~18.2.45",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"expo": "~50.0.11",
|
"expo": "~50.0.11",
|
||||||
"expo-constants": "~15.4.5",
|
"expo-constants": "~15.4.5",
|
||||||
"expo-linking": "~6.2.2",
|
"expo-linking": "~6.2.2",
|
||||||
"expo-router": "~3.4.8",
|
"expo-router": "~3.4.8",
|
||||||
"expo-status-bar": "~1.11.1",
|
"expo-status-bar": "~1.11.1",
|
||||||
|
"hono": "^4.1.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.73.4",
|
"react-native": "0.73.4",
|
||||||
|
"react-native-circular-progress": "^1.3.9",
|
||||||
"react-native-safe-area-context": "4.8.2",
|
"react-native-safe-area-context": "4.8.2",
|
||||||
"react-native-screens": "~3.29.0",
|
"react-native-screens": "~3.29.0",
|
||||||
|
"react-native-svg": "14.1.0",
|
||||||
"react-native-web": "~0.19.6",
|
"react-native-web": "~0.19.6",
|
||||||
"react-query": "^3.39.3",
|
"react-query": "^3.39.3",
|
||||||
"twrnc": "^4.1.0",
|
"twrnc": "^4.1.0",
|
||||||
@ -29,7 +34,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
"babel-plugin-module-resolver": "^5.0.0"
|
"babel-plugin-module-resolver": "^5.0.0",
|
||||||
|
"react-native-svg-transformer": "^1.3.0"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
460
pnpm-lock.yaml
generated
460
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,18 +3,26 @@ import { Slot } from "expo-router";
|
|||||||
import { QueryClientProvider } from "react-query";
|
import { QueryClientProvider } from "react-query";
|
||||||
import queryClient from "@/lib/queryClient";
|
import queryClient from "@/lib/queryClient";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, tw } from "@/lib/utils";
|
||||||
|
import { useDeviceContext } from "twrnc";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
const RootLayout = () => {
|
const RootLayout = () => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
useDeviceContext(tw);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
<View style={cn("flex-1 bg-white", { paddingTop: insets.top })}>
|
<View style={cn("flex-1 bg-[#f2f7fb]")}>
|
||||||
<Slot />
|
<View
|
||||||
|
style={cn("flex-1 mx-auto w-full max-w-xl", {
|
||||||
|
paddingTop: insets.top,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Slot />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
102
src/app/_sections/Performance.tsx
Normal file
102
src/app/_sections/Performance.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { AnimatedCircularProgress } from "react-native-circular-progress";
|
||||||
|
import { InferResponseType } from "hono/client";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
import Text from "@ui/Text";
|
||||||
|
import { HStack, VStack } from "@ui/Stack";
|
||||||
|
import Box from "@ui/Box";
|
||||||
|
import ProcessList from "./ProcessList";
|
||||||
|
import Divider from "@ui/Divider";
|
||||||
|
import Button from "@ui/Button";
|
||||||
|
import { Ionicons } from "@ui/Icons";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: InferResponseType<typeof api.system.$get>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Performance = ({ data: system }: Props) => {
|
||||||
|
const [showProcess, setShowProcess] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text className="text-lg font-medium mt-8">Performance</Text>
|
||||||
|
|
||||||
|
<Box className="px-4 py-6 mt-2 bg-white border border-gray-100 rounded-lg relative">
|
||||||
|
<HStack className="justify-evenly">
|
||||||
|
<VStack className="items-center">
|
||||||
|
<AnimatedCircularProgress
|
||||||
|
size={120}
|
||||||
|
width={15}
|
||||||
|
backgroundWidth={5}
|
||||||
|
fill={system?.perf.cpu.load || 0}
|
||||||
|
tintColor="#6366F1"
|
||||||
|
backgroundColor="#3d5875"
|
||||||
|
arcSweepAngle={240}
|
||||||
|
rotation={240}
|
||||||
|
lineCap="round"
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<Text>
|
||||||
|
<Text className="text-2xl mr-0.5">
|
||||||
|
{Math.round(system?.perf.cpu.load || 0)}
|
||||||
|
</Text>
|
||||||
|
%
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</AnimatedCircularProgress>
|
||||||
|
<Text className="-mt-8 text-lg">CPU</Text>
|
||||||
|
{system ? (
|
||||||
|
<Text className="text-xs">
|
||||||
|
{`${system.perf.cpu.speed.toFixed(1)} GHz / ${
|
||||||
|
system.perf.cpu.temp
|
||||||
|
}°C`}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</VStack>
|
||||||
|
<VStack className="items-center">
|
||||||
|
<AnimatedCircularProgress
|
||||||
|
size={120}
|
||||||
|
width={15}
|
||||||
|
backgroundWidth={5}
|
||||||
|
fill={system?.perf.mem.percent || 0}
|
||||||
|
tintColor="#6366F1"
|
||||||
|
backgroundColor="#3d5875"
|
||||||
|
arcSweepAngle={240}
|
||||||
|
rotation={240}
|
||||||
|
lineCap="round"
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<Text>
|
||||||
|
<Text className="text-2xl mr-0.5">
|
||||||
|
{Math.round(system?.perf.mem.percent || 0)}
|
||||||
|
</Text>
|
||||||
|
%
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</AnimatedCircularProgress>
|
||||||
|
<Text className="-mt-8 text-lg">Mem</Text>
|
||||||
|
<Text className="text-xs">{system?.perf.mem.used}</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-forward"
|
||||||
|
style={{
|
||||||
|
transform: showProcess ? [{ rotate: "90deg" }] : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="absolute right-0 top-1"
|
||||||
|
variant="ghost"
|
||||||
|
onPress={() => setShowProcess(!showProcess)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showProcess && <ProcessList />}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Performance;
|
54
src/app/_sections/ProcessList.tsx
Normal file
54
src/app/_sections/ProcessList.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import api from "@/lib/api";
|
||||||
|
import Box from "@ui/Box";
|
||||||
|
import Button from "@ui/Button";
|
||||||
|
import { HStack, VStack } from "@ui/Stack";
|
||||||
|
import Text from "@ui/Text";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
|
const ProcessList = () => {
|
||||||
|
const [sort, setSort] = useState<string>("mem");
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["process", sort],
|
||||||
|
queryFn: async () => {
|
||||||
|
return api.process
|
||||||
|
.$get({ query: { sort, limit: 5 } })
|
||||||
|
.then((r) => r.json());
|
||||||
|
},
|
||||||
|
select: (i) => i.list,
|
||||||
|
refetchInterval: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="mt-4">
|
||||||
|
<HStack className="gap-2 flex-wrap">
|
||||||
|
<Button
|
||||||
|
label="Mem"
|
||||||
|
variant={sort === "mem" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onPress={() => setSort("mem")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="CPU"
|
||||||
|
variant={sort === "cpu" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onPress={() => setSort("cpu")}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<VStack className="gap-2 mt-3">
|
||||||
|
{data?.map((item, idx) => (
|
||||||
|
<HStack key={idx} className="pb-2 border-b border-gray-200">
|
||||||
|
<Text className="flex-1" numberOfLines={1}>
|
||||||
|
{item.cmd}
|
||||||
|
</Text>
|
||||||
|
<Text>{sort === "mem" ? item.memUsage : item.cpuPercent}</Text>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProcessList;
|
49
src/app/_sections/Storage.tsx
Normal file
49
src/app/_sections/Storage.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { InferResponseType } from "hono/client";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
import Text from "@ui/Text";
|
||||||
|
import Box from "@ui/Box";
|
||||||
|
import DriveIcon from "@/assets/icons/harddrive.svg";
|
||||||
|
import { HStack, VStack } from "@ui/Stack";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: InferResponseType<typeof api.system.$get>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Storage = ({ data }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text className="text-lg font-medium mt-8">Storage</Text>
|
||||||
|
<HStack className="px-1 py-4 mt-2 bg-white border border-gray-100 rounded-lg gap-3 flex-wrap">
|
||||||
|
{data?.storage.map((item) => (
|
||||||
|
<Box key={item.mount} className="flex-1 basis-full sm:basis-[40%]">
|
||||||
|
<HStack className="flex items-center justify-center gap-2">
|
||||||
|
<DriveIcon style={{ width: 72, height: 72 }} />
|
||||||
|
<VStack className="flex-1 gap-1">
|
||||||
|
<Text className="text-primary font-bold">{item.mount}</Text>
|
||||||
|
<Text>{`Total: ${item.total}`}</Text>
|
||||||
|
<Text>{`Free: ${item.free}`}</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Box className="rounded-full h-2 mx-4 bg-gray-200 overflow-hidden">
|
||||||
|
<Box
|
||||||
|
className={[
|
||||||
|
"rounded-full h-2",
|
||||||
|
item.percent > 90
|
||||||
|
? "bg-red-500"
|
||||||
|
: item.percent > 75
|
||||||
|
? "bg-yellow-500"
|
||||||
|
: "bg-primary-400",
|
||||||
|
]}
|
||||||
|
style={{ width: `${item.percent}%` }}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Storage;
|
24
src/app/_sections/Summary.tsx
Normal file
24
src/app/_sections/Summary.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Box from "@ui/Box";
|
||||||
|
import Text from "@ui/Text";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { InferResponseType } from "hono/client";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: InferResponseType<typeof api.system.$get>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Summary = ({ data }: Props) => {
|
||||||
|
return (
|
||||||
|
<Box className="px-4 py-6 mt-4 bg-white border border-gray-100 rounded-lg">
|
||||||
|
<Text className="text-5xl">{dayjs(data?.date).format("HH:mm")}</Text>
|
||||||
|
<Text className="mt-2">
|
||||||
|
{dayjs(data?.date).format("dddd, DD MMM YYYY")}
|
||||||
|
</Text>
|
||||||
|
<Text className="flex-1">{`Uptime: ${data?.uptime || "-"}`}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Summary;
|
@ -1,26 +1,31 @@
|
|||||||
import { Text } from "react-native";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import Text from "@ui/Text";
|
||||||
|
import Performance from "./_sections/Performance";
|
||||||
|
import Summary from "./_sections/Summary";
|
||||||
|
import Storage from "./_sections/Storage";
|
||||||
|
import { ScrollView } from "react-native";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import useAPI from "@/hooks/useAPI";
|
|
||||||
import { Ionicons } from "@ui/Icons";
|
|
||||||
import { VStack } from "@ui/Stack";
|
|
||||||
import Button from "@ui/Button";
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { data } = useAPI("/posts/1");
|
const { data: system } = useQuery({
|
||||||
|
queryKey: ["system"],
|
||||||
|
queryFn: () => api.system.$get().then((r) => r.json()),
|
||||||
|
refetchInterval: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack className="gap-3 p-4">
|
<ScrollView
|
||||||
<Text style={cn("text-2xl font-medium")}>App</Text>
|
contentContainerStyle={cn("px-4 py-8 md:py-16")}
|
||||||
<Text style={cn("w-full")}>{data?.body}</Text>
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<Text className="text-2xl font-medium">Home Lab</Text>
|
||||||
|
|
||||||
<Button label="Click me" />
|
<Summary data={system} />
|
||||||
<Button label="Click me" variant="secondary" />
|
<Performance data={system} />
|
||||||
<Button label="Click me" variant="ghost" />
|
<Storage data={system} />
|
||||||
<Button label="Click me" variant="outline" />
|
</ScrollView>
|
||||||
<Button label="Click me" variant="destructive" />
|
|
||||||
<Button icon={<Ionicons name="trash" />} size="icon" />
|
|
||||||
</VStack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
12
src/assets/icons/harddrive.svg
Normal file
12
src/assets/icons/harddrive.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 48 48">
|
||||||
|
<g>
|
||||||
|
<path d="m 12.563 7.5 l 22.875 0 c 1.141 0 2.063 0.922 2.063 2.063 l 0 28.875 c 0 1.141 -0.922 2.063 -2.063 2.063 l -22.875 0 c -1.141 0 -2.063 -0.922 -2.063 -2.063 l 0 -28.875 c 0 -1.141 0.922 -2.063 2.063 -2.063 m 0 0" style="fill:#829495;fill-opacity:1;stroke:none;fill-rule:nonzero"/>
|
||||||
|
<path d="m 30.01 7.5 l 4.5 0 l 0 33 l -4.5 0 m 0 -33" style="fill:#fcf5e3;fill-opacity:0.463;stroke:none;fill-rule:nonzero"/>
|
||||||
|
<path d="m 24.09 11.965 c -1.453 -0.016 -2.926 0.316 -4.297 1.043 c -4.395 2.328 -6.03 7.77 -3.699 12.164 c 1.93 3.641 5.984 5.402 9.828 4.582 l -1.676 -5.91 c -0.055 0.004 -0.102 0.031 -0.156 0.031 c -1.676 0 -3.035 -1.355 -3.035 -3.03 c 0 -1.676 1.359 -3.035 3.035 -3.035 c 1.672 0 3.03 1.359 3.03 3.035 c 0 0.75 -0.277 1.43 -0.727 1.957 l 3.887 4.645 c 2.816 -2.719 3.629 -7.078 1.703 -10.711 c -1.598 -3.02 -4.703 -4.734 -7.895 -4.77 m 0 0" style="fill:#063642;fill-opacity:1;stroke:none;fill-rule:nonzero"/>
|
||||||
|
<path d="m 26.805 26.719 l 3.094 9.281 l 3.094 0 l 0 -2.063 m -6.188 -7.219" style="fill:#063642;fill-opacity:1;stroke:none;fill-rule:nonzero"/>
|
||||||
|
<path d="m 15.527 23.621 c 0.805 2.531 2.738 4.629 5.316 5.637 l 2.121 -5.645 c -0.309 -0.121 -0.582 -0.293 -0.848 -0.52 c -0.426 -0.367 -0.734 -0.824 -0.902 -1.32 m -5.688 1.848" style="fill:#93a1a1;fill-opacity:0.494;stroke:none;fill-rule:nonzero"/>
|
||||||
|
<path d="m 15.27 18.992 c -0.566 2.594 0.059 5.379 1.789 7.543 l 4.652 -3.828 c -0.203 -0.262 -0.355 -0.547 -0.469 -0.875 c -0.188 -0.531 -0.227 -1.082 -0.121 -1.598 m -5.852 -1.242" style="fill:#268bd1;fill-opacity:0.247;stroke:none;fill-rule:nonzero"/>
|
||||||
|
<path d="m 32.523 17.969 c -0.824 -2.523 -2.77 -4.605 -5.359 -5.598 l -2.078 5.66 c 0.309 0.117 0.586 0.289 0.852 0.512 c 0.426 0.363 0.738 0.82 0.91 1.316 m 5.676 -1.891" style="fill:#93a1a1;fill-opacity:0.494;stroke:none;fill-rule:nonzero"/>
|
||||||
|
<path d="m 32.816 22.598 c 0.547 -2.598 -0.098 -5.379 -1.844 -7.527 l -4.629 3.859 c 0.207 0.258 0.363 0.543 0.48 0.871 c 0.188 0.527 0.23 1.082 0.129 1.594 m 5.863 1.203" style="fill:#268bd1;fill-opacity:0.192;stroke:none;fill-rule:nonzero"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
@ -4,8 +4,10 @@ import { View } from "react-native";
|
|||||||
|
|
||||||
type Props = ComponentPropsWithClassName<typeof View>;
|
type Props = ComponentPropsWithClassName<typeof View>;
|
||||||
|
|
||||||
const Box = ({ className, ...props }: Props) => {
|
const Box = ({ className, style, ...props }: Props) => {
|
||||||
return <View style={cn(className)} {...props} />;
|
return (
|
||||||
|
<View style={{ ...cn(className), ...((style || {}) as any) }} {...props} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Box;
|
export default Box;
|
||||||
|
9
src/components/ui/Divider.tsx
Normal file
9
src/components/ui/Divider.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { View, Text } from "react-native";
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Divider = ({ className }: { className?: string }) => {
|
||||||
|
return <View style={cn("border-b border-gray-300 w-full h-1", className)} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Divider;
|
13
src/components/ui/Text.tsx
Normal file
13
src/components/ui/Text.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Text as BaseText } from "react-native";
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ComponentPropsWithClassName } from "@/types/components";
|
||||||
|
|
||||||
|
const Text = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComponentPropsWithClassName<typeof BaseText>) => {
|
||||||
|
return <BaseText style={cn(className)} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Text;
|
@ -1,11 +0,0 @@
|
|||||||
import api, { APIParams } from "@/lib/api";
|
|
||||||
import { useQuery } from "react-query";
|
|
||||||
|
|
||||||
const useAPI = <T = any>(url: string, params?: APIParams) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: [url, params],
|
|
||||||
queryFn: async () => api<T>(url, { params }),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useAPI;
|
|
@ -1,75 +1,20 @@
|
|||||||
import { API_BASEURL } from "./constants";
|
import { API_BASEURL } from "./constants";
|
||||||
|
import { hc } from "hono/dist/client";
|
||||||
|
import type {
|
||||||
|
hc as hono,
|
||||||
|
InferRequestType,
|
||||||
|
InferResponseType,
|
||||||
|
ClientRequestOptions,
|
||||||
|
ClientResponse,
|
||||||
|
} from "hono/client";
|
||||||
|
import type { AppType } from "../../backend";
|
||||||
|
|
||||||
export type APIParams = { [key: string]: any };
|
const api = hc(API_BASEURL) as ReturnType<typeof hono<AppType>>;
|
||||||
|
|
||||||
export type APIOptions = RequestInit & {
|
export {
|
||||||
params?: APIParams;
|
InferRequestType,
|
||||||
|
InferResponseType,
|
||||||
|
ClientRequestOptions,
|
||||||
|
ClientResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
const api = async <T>(url: string, options: APIOptions = {}): Promise<T> => {
|
|
||||||
const fullUrl = new URL(url, API_BASEURL);
|
|
||||||
|
|
||||||
if (options.params && Object.keys(options.params).length > 0) {
|
|
||||||
for (const [key, value] of Object.entries(options.params)) {
|
|
||||||
fullUrl.searchParams.append(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(fullUrl.toString(), options);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new APIError(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
||||||
const data = (await response.json()) as T;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.text();
|
|
||||||
return data as T;
|
|
||||||
};
|
|
||||||
|
|
||||||
api.post = async <T>(url: string, body: any, options: APIOptions = {}) => {
|
|
||||||
return api<T>(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
api.patch = async <T>(url: string, body: any, options: APIOptions = {}) => {
|
|
||||||
return api<T>(url, {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
api.put = async <T>(url: string, body: any, options: APIOptions = {}) => {
|
|
||||||
return api<T>(url, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
api.delete = async <T>(url: string, options: APIOptions = {}) => {
|
|
||||||
return api<T>(url, {
|
|
||||||
method: "DELETE",
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export class APIError extends Error {
|
|
||||||
res: Response;
|
|
||||||
code: number;
|
|
||||||
|
|
||||||
constructor(res: Response) {
|
|
||||||
super(res.statusText);
|
|
||||||
this.res = res;
|
|
||||||
this.code = res.status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
@ -1,2 +1 @@
|
|||||||
// export const API_BASEURL = "http://localhost:8000";
|
export const API_BASEURL = "http://localhost:3000";
|
||||||
export const API_BASEURL = "https://jsonplaceholder.typicode.com";
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ClassInput, create as createTwrnc } from "twrnc";
|
import { ClassInput, create as createTwrnc } from "twrnc";
|
||||||
|
|
||||||
const tw = createTwrnc(require(`../../tailwind.config.js`));
|
export const tw = createTwrnc(require(`../../tailwind.config.js`));
|
||||||
|
|
||||||
export const cn = (...args: ClassInput[]) => {
|
export const cn = (...args: ClassInput[]) => {
|
||||||
if (Array.isArray(args[0])) {
|
if (Array.isArray(args[0])) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user