Files
zmb-webui/frontend/lib/api.ts
T
patrick eec74626d0 Improve Samba Global Configuration display in WebUI
Backend:
- Parse global config into structured key-value pairs
- Return parameters array instead of raw text
- Better handling of comments and empty lines

Frontend:
- Add 'Samba Config' tab to shares page
- Display config parameters in readable table format
- Color-code parameter names for clarity
- Add getSambaConfig() method to API client

Co-Authored-By: Patrick <patrick@perlbach24.de>
2026-04-22 01:01:58 +02:00

486 lines
14 KiB
TypeScript

import axios, { AxiosInstance } from "axios"
export interface Pool {
name: string
size: number
alloc: number
free: number
fragmentation: string
capacity: string
health: "ONLINE" | "DEGRADED" | "FAULTED" | "OFFLINE" | "UNAVAIL"
}
export interface Vdev {
name: string
state: string
read: number
write: number
cksum: number
children?: Vdev[]
}
export interface PoolStatus extends Pool {
state?: string
scan?: string
errors?: string
vdevs: Vdev[]
}
export interface Dataset {
name: string
type: "filesystem" | "volume" | "snapshot"
used: number
avail: number
refer: number
mountpoint?: string
compression?: string
quota?: number
reservation?: number
}
export interface DatasetProperties {
compression?: string
quota?: number
reservation?: number
}
export interface Snapshot {
name: string
dataset?: string
creation: number
used: number
referenced: number
}
export interface SambaShare {
name: string
path: string
comment?: string
read_only?: boolean
guest_ok?: boolean
valid_users?: string
}
export interface NfsShare {
path: string
clients: string
options?: string
}
export interface SystemInfo {
hostname: string
uptime: number
memory_total: number
memory_used: number
memory_available: number
cpu_count: number
cpu_model: string
os: string
}
export interface SystemStatus {
status: string
zfs_available: boolean
version: string
}
export interface SystemUser {
username: string
uid: number
gid: number
home: string
shell: string
gecos?: string
locked?: boolean
groups: string[]
}
export interface SystemGroup {
groupname: string
gid: number
members: string[]
}
export interface LoginEntry {
user: string
terminal: string
host: string
login_time: string
logout_time?: string
duration?: string
}
export class ZFSManagerAPI {
private client: AxiosInstance
private token: string | null = null
constructor(baseURL: string = process.env.NEXT_PUBLIC_API_URL || "") {
this.client = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
},
})
if (typeof window !== "undefined") {
this.token = localStorage.getItem("access_token")
if (this.token) {
this.setAuthHeader()
}
}
this.client.interceptors.response.use(
(response) => response,
(error) => {
// Don't redirect on login endpoint itself
const isLoginEndpoint = error.config?.url?.includes("/api/auth/login")
if (error.response?.status === 401 && !isLoginEndpoint) {
localStorage.removeItem("access_token")
this.token = null
if (typeof window !== "undefined") {
window.location.href = "/login"
}
}
return Promise.reject(error)
}
)
}
private setAuthHeader() {
if (this.token) {
this.client.defaults.headers.common["Authorization"] = `Bearer ${this.token}`
}
}
// Auth
async login(username: string, password: string): Promise<{ access_token: string; token_type: string }> {
// Clear any existing token before login
localStorage.removeItem("access_token")
this.token = null
this.client.defaults.headers.common["Authorization"] = ""
try {
const response = await this.client.post("/api/auth/login", { username, password })
this.token = response.data.access_token
this.setAuthHeader()
if (this.token) {
localStorage.setItem("access_token", this.token)
}
return response.data
} catch (error: any) {
// Clear any invalid token on login failure
localStorage.removeItem("access_token")
this.token = null
this.client.defaults.headers.common["Authorization"] = ""
// Provide better error message
const message = error?.response?.data?.detail || error?.message || "Login failed"
const err = new Error(message)
throw err
}
}
async logout() {
this.token = null
this.client.defaults.headers.common["Authorization"] = ""
localStorage.removeItem("access_token")
}
async verifyToken(): Promise<{ valid: boolean; username: string }> {
try {
const response = await this.client.post("/api/auth/verify")
return response.data
} catch {
return { valid: false, username: "" }
}
}
// System Status (no auth required)
async getSystemStatus(): Promise<SystemStatus> {
try {
const response = await this.client.get("/api/status")
return response.data
} catch {
return { status: "unknown", zfs_available: false, version: "" }
}
}
// Pools
async getPools(): Promise<Pool[]> {
const response = await this.client.get("/api/pools/")
return response.data
}
async getPoolStatus(name: string): Promise<PoolStatus> {
const response = await this.client.get(`/api/pools/${name}`)
return response.data
}
async startScrub(poolName: string): Promise<{ status: string }> {
const response = await this.client.post(`/api/pools/${poolName}/scrub`)
return response.data
}
// Datasets
async getDatasets(pool: string = "tank"): Promise<Dataset[]> {
const response = await this.client.get("/api/datasets/", { params: { pool } })
return response.data
}
async createDataset(name: string, properties?: Record<string, string>): Promise<{ status: string }> {
const response = await this.client.post("/api/datasets/", { name, properties })
return response.data
}
async updateDatasetProperties(name: string, props: DatasetProperties): Promise<{ status: string }> {
const response = await this.client.patch(`/api/datasets/${name}`, props)
return response.data
}
async deleteDataset(name: string, recursive = false): Promise<{ status: string }> {
const response = await this.client.delete(`/api/datasets/${name}`, { params: { recursive } })
return response.data
}
// Snapshots
async getSnapshots(dataset?: string, limit = 50): Promise<Snapshot[]> {
const response = await this.client.get("/api/snapshots/", {
params: { ...(dataset ? { dataset } : {}), limit },
})
return response.data
}
async createSnapshot(dataset: string, name?: string): Promise<{ status: string }> {
const response = await this.client.post("/api/snapshots/", { dataset, name })
return response.data
}
async deleteSnapshot(name: string): Promise<{ status: string }> {
const response = await this.client.delete(`/api/snapshots/${name}`)
return response.data
}
async rollbackSnapshot(snapshot: string): Promise<{ status: string }> {
const response = await this.client.post("/api/snapshots/rollback", { snapshot })
return response.data
}
// Shares — Samba
async getSambaShares(): Promise<SambaShare[]> {
const response = await this.client.get("/api/shares/samba")
return response.data.shares ?? response.data
}
async createSambaShare(share: SambaShare): Promise<{ status: string }> {
const response = await this.client.post("/api/shares/samba", share)
return response.data
}
async deleteSambaShare(name: string): Promise<{ status: string }> {
const response = await this.client.delete(`/api/shares/samba/${name}`)
return response.data
}
async getSambaConfig(): Promise<{ parameters: Array<{ key: string; value: string }> }> {
const response = await this.client.get("/api/shares/samba/config")
return response.data
}
// Shares — NFS
async getNfsShares(): Promise<NfsShare[]> {
const response = await this.client.get("/api/shares/nfs")
return response.data.shares ?? response.data
}
async createNfsShare(share: NfsShare): Promise<{ status: string }> {
const response = await this.client.post("/api/shares/nfs", share)
return response.data
}
async deleteNfsShare(path: string): Promise<{ status: string }> {
const response = await this.client.delete("/api/shares/nfs", { params: { path } })
return response.data
}
// Shares Configuration
async getSambaGlobalConfig(): Promise<{ [key: string]: any }> {
const response = await this.client.get("/api/shares/samba/config")
return response.data
}
async setSambaGlobalConfig(config: string): Promise<{ status: string }> {
const response = await this.client.put("/api/shares/samba/config", { config })
return response.data
}
async getNfsGlobalConfig(): Promise<{ exports: string; path?: string }> {
const response = await this.client.get("/api/shares/nfs/config")
return response.data
}
async setNfsGlobalConfig(config: string): Promise<{ status: string }> {
const response = await this.client.put("/api/shares/nfs/config", { config })
return response.data
}
// System
async getSystemInfo(): Promise<SystemInfo> {
const response = await this.client.get("/api/system/info")
return response.data
}
async getMemory(): Promise<{ total: number; used: number; available: number; swap_total: number; swap_used: number }> {
const response = await this.client.get("/api/system/memory")
return response.data
}
async getCpuInfo(): Promise<{ count: number; percent?: number; load_average: number[] }> {
const response = await this.client.get("/api/system/cpu")
return response.data
}
async getUptime(): Promise<{ uptime_seconds: number; uptime_string: string }> {
const response = await this.client.get("/api/system/uptime")
return response.data
}
async getNetwork(): Promise<{ interfaces: any[] }> {
const response = await this.client.get("/api/system/network")
return response.data
}
async getNetworkTraffic(): Promise<{ interfaces: any[] }> {
const response = await this.client.get("/api/system/network/traffic")
return response.data
}
async getDiskIO(): Promise<{ disks: any[] }> {
const response = await this.client.get("/api/system/diskio")
return response.data
}
async getServices(): Promise<{ services: any[] }> {
const response = await this.client.get("/api/system/services")
return response.data
}
async getUnits(): Promise<{
services: any[]
targets: any[]
sockets: any[]
timers: any[]
paths: any[]
}> {
const response = await this.client.get("/api/system/units")
return response.data
}
async getSystemLogs(limit: number = 20): Promise<{ logs: string[] }> {
const response = await this.client.get(`/api/system/logs?limit=${limit}`)
return response.data
}
async getHealth(): Promise<{ status: string; version: string }> {
const response = await this.client.get("/health")
return response.data
}
// Identities - Users
async getUsers(): Promise<SystemUser[]> {
const response = await this.client.get("/api/identities/users")
return response.data.users ?? []
}
async createUser(username: string, home_dir?: string, shell?: string, gecos?: string): Promise<{ status: string }> {
const response = await this.client.post("/api/identities/users", { username, home_dir, shell, gecos })
return response.data
}
async deleteUser(username: string, remove_home: boolean = true): Promise<{ status: string }> {
const response = await this.client.delete(`/api/identities/users/${username}`, { params: { remove_home } })
return response.data
}
async changePassword(username: string, password: string): Promise<{ status: string }> {
const response = await this.client.post(`/api/identities/users/${username}/password`, { password })
return response.data
}
async changeShell(username: string, shell: string): Promise<{ status: string }> {
const response = await this.client.post(`/api/identities/users/${username}/shell`, { shell })
return response.data
}
async lockUser(username: string): Promise<{ status: string }> {
const response = await this.client.post(`/api/identities/users/${username}/lock`)
return response.data
}
async unlockUser(username: string): Promise<{ status: string }> {
const response = await this.client.post(`/api/identities/users/${username}/unlock`)
return response.data
}
async setSambaPassword(username: string, password: string): Promise<{ status: string }> {
const response = await this.client.post(`/api/identities/users/${username}/samba-password`, { password })
return response.data
}
// Identities - Groups
async getGroups(): Promise<SystemGroup[]> {
const response = await this.client.get("/api/identities/groups")
return response.data.groups ?? []
}
async createGroup(groupname: string): Promise<{ status: string }> {
const response = await this.client.post("/api/identities/groups", { groupname })
return response.data
}
async deleteGroup(groupname: string): Promise<{ status: string }> {
const response = await this.client.delete(`/api/identities/groups/${groupname}`)
return response.data
}
async addUserToGroup(username: string, groupname: string): Promise<{ status: string }> {
const response = await this.client.post(`/api/identities/users/${username}/groups`, { groupname })
return response.data
}
async removeUserFromGroup(username: string, groupname: string): Promise<{ status: string }> {
const response = await this.client.delete(`/api/identities/users/${username}/groups/${groupname}`)
return response.data
}
// Identities - Samba Users
async getSambaUsers(): Promise<SystemUser[]> {
const response = await this.client.get("/api/identities/samba-users")
return response.data.users ?? []
}
// Identities - Login History
async getLoginHistory(limit: number = 50): Promise<LoginEntry[]> {
const response = await this.client.get("/api/identities/login-history", { params: { limit } })
return response.data.logins ?? []
}
// Navigator - Copy, Move, Search
async copyFile(src: string, dst: string, overwrite: boolean = false): Promise<{ status: string }> {
const response = await this.client.post("/api/navigator/copy", { src, dst, overwrite })
return response.data
}
async moveFile(src: string, dst: string, overwrite: boolean = false): Promise<{ status: string }> {
const response = await this.client.post("/api/navigator/move", { src, dst, overwrite })
return response.data
}
async searchFiles(q: string, path: string = "", limit: number = 50): Promise<any[]> {
const response = await this.client.get("/api/navigator/search", { params: { q, path, limit } })
return response.data.results ?? []
}
}
export const api = new ZFSManagerAPI()