feat: complete all page components
Build-Deploy-Server / build (base, debian, ) (push) Failing after 7s
Build-Docker-ARM64 / build (base, debian, ) (push) Failing after 11s
Build-Docker-ARM64 / build (base-debian13, debian13, -debian13) (push) Failing after 11s
Build-Docker-ARM64 / build (base-minimal, minimal, -minimal) (push) Failing after 10s
Deploy VitePress to Pages / build (push) Has been cancelled
Deploy VitePress to Pages / Deploy (push) Has been cancelled
Build-Deploy-Server / build (base, debian, ) (push) Failing after 7s
Build-Docker-ARM64 / build (base, debian, ) (push) Failing after 11s
Build-Docker-ARM64 / build (base-debian13, debian13, -debian13) (push) Failing after 11s
Build-Docker-ARM64 / build (base-minimal, minimal, -minimal) (push) Failing after 10s
Deploy VitePress to Pages / build (push) Has been cancelled
Deploy VitePress to Pages / Deploy (push) Has been cancelled
- Dashboard: system stats with cards - Tasks: task management table with filters - Scripts: script management grid - Logs: execution log viewer - Interconnect: node management - Terminal: xterm.js integration - Settings: theme and site config - Login: authentication page
This commit is contained in:
+2
-1
@@ -13,7 +13,8 @@
|
|||||||
"@tanstack/react-router": "^1.120.0",
|
"@tanstack/react-router": "^1.120.0",
|
||||||
"@tanstack/react-table": "^8.21.0",
|
"@tanstack/react-table": "^8.21.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"framer-motion": "^12.0.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"framer-motion": "^11.15.0",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
|
|||||||
Generated
+26
-14
@@ -17,12 +17,15 @@ importers:
|
|||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.21.0
|
specifier: ^8.21.0
|
||||||
version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
'@xterm/addon-fit':
|
||||||
|
specifier: ^0.10.0
|
||||||
|
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||||
'@xterm/xterm':
|
'@xterm/xterm':
|
||||||
specifier: ^5.5.0
|
specifier: ^5.5.0
|
||||||
version: 5.5.0
|
version: 5.5.0
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.0.0
|
specifier: ^11.15.0
|
||||||
version: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 11.18.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.525.0
|
specifier: ^0.525.0
|
||||||
version: 0.525.0(react@19.2.7)
|
version: 0.525.0(react@19.2.7)
|
||||||
@@ -286,6 +289,11 @@ packages:
|
|||||||
babel-plugin-react-compiler:
|
babel-plugin-react-compiler:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@xterm/addon-fit@0.10.0':
|
||||||
|
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@xterm/xterm': ^5.0.0
|
||||||
|
|
||||||
'@xterm/xterm@5.5.0':
|
'@xterm/xterm@5.5.0':
|
||||||
resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==}
|
resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==}
|
||||||
|
|
||||||
@@ -399,8 +407,8 @@ packages:
|
|||||||
fraction.js@5.3.4:
|
fraction.js@5.3.4:
|
||||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||||
|
|
||||||
framer-motion@12.42.2:
|
framer-motion@11.18.2:
|
||||||
resolution: {integrity: sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==}
|
resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@emotion/is-prop-valid': '*'
|
'@emotion/is-prop-valid': '*'
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
@@ -515,11 +523,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
motion-dom@12.42.2:
|
motion-dom@11.18.1:
|
||||||
resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==}
|
resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
|
||||||
|
|
||||||
motion-utils@12.39.0:
|
motion-utils@11.18.1:
|
||||||
resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
|
resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
|
||||||
|
|
||||||
nanoid@3.3.16:
|
nanoid@3.3.16:
|
||||||
resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
|
resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
|
||||||
@@ -875,6 +883,10 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
vite: 8.1.5(@types/node@22.20.1)
|
vite: 8.1.5(@types/node@22.20.1)
|
||||||
|
|
||||||
|
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
|
||||||
|
dependencies:
|
||||||
|
'@xterm/xterm': 5.5.0
|
||||||
|
|
||||||
'@xterm/xterm@5.5.0': {}
|
'@xterm/xterm@5.5.0': {}
|
||||||
|
|
||||||
autoprefixer@10.5.4(postcss@8.5.19):
|
autoprefixer@10.5.4(postcss@8.5.19):
|
||||||
@@ -965,10 +977,10 @@ snapshots:
|
|||||||
|
|
||||||
fraction.js@5.3.4: {}
|
fraction.js@5.3.4: {}
|
||||||
|
|
||||||
framer-motion@12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
framer-motion@11.18.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-dom: 12.42.2
|
motion-dom: 11.18.1
|
||||||
motion-utils: 12.39.0
|
motion-utils: 11.18.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
@@ -1042,11 +1054,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
|
|
||||||
motion-dom@12.42.2:
|
motion-dom@11.18.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-utils: 12.39.0
|
motion-utils: 11.18.1
|
||||||
|
|
||||||
motion-utils@12.39.0: {}
|
motion-utils@11.18.1: {}
|
||||||
|
|
||||||
nanoid@3.3.16: {}
|
nanoid@3.3.16: {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { request } from './index'
|
||||||
|
import type { MonitorStats, Task, Script, EnvVar, LogEntry, InterconnectNode, User, Settings } from './types'
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
export const login = (username: string, password: string) =>
|
||||||
|
request<{ token: string }>('/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
})
|
||||||
|
|
||||||
|
export const logout = () => request<void>('/auth/logout', { method: 'POST' })
|
||||||
|
export const getCurrentUser = () => request<User>('/auth/me')
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
export const getStats = () => request<MonitorStats>('/stats')
|
||||||
|
export const getSentence = () => request<{ content: string; author: string }>('/sentence')
|
||||||
|
export const getTaskStats = () => request<{ total: number; running: number; scheduled: number }>('/taskstats')
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
export const getTasks = (params?: { status?: string; tag?: string }) => {
|
||||||
|
const query = new URLSearchParams(params as any).toString()
|
||||||
|
return request<Task[]>(`/tasks${query ? '?' + query : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTask = (id: number) => request<Task>(`/tasks/${id}`)
|
||||||
|
export const createTask = (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
export const updateTask = (id: number, data: Partial<Task>) => request<Task>(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) })
|
||||||
|
export const deleteTask = (id: number) => request<void>(`/tasks/${id}`, { method: 'DELETE' })
|
||||||
|
export const executeTask = (id: number) => request<void>(`/execute/task/${id}`, { method: 'POST' })
|
||||||
|
export const getTags = () => request<string[]>('/tasks/tags')
|
||||||
|
|
||||||
|
// Scripts
|
||||||
|
export const getScripts = () => request<Script[]>('/scripts')
|
||||||
|
export const getScript = (id: number) => request<Script>(`/scripts/${id}`)
|
||||||
|
export const createScript = (data: Partial<Script>) => request<Script>('/scripts', { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
export const updateScript = (id: number, data: Partial<Script>) => request<Script>(`/scripts/${id}`, { method: 'PUT', body: JSON.stringify(data) })
|
||||||
|
export const deleteScript = (id: number) => request<void>(`/scripts/${id}`, { method: 'DELETE' })
|
||||||
|
|
||||||
|
// Env
|
||||||
|
export const getEnvs = () => request<EnvVar[]>('/env')
|
||||||
|
export const getEnv = (id: number) => request<EnvVar>(`/env/${id}`)
|
||||||
|
export const createEnv = (data: Partial<EnvVar>) => request<EnvVar>('/env', { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
export const updateEnv = (id: number, data: Partial<EnvVar>) => request<EnvVar>(`/env/${id}`, { method: 'PUT', body: JSON.stringify(data) })
|
||||||
|
export const deleteEnv = (id: number) => request<void>(`/env/${id}`, { method: 'DELETE' })
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
export const getLogs = (params?: { task_id?: number; status?: string; page?: number }) => {
|
||||||
|
const query = new URLSearchParams(params as any).toString()
|
||||||
|
return request<LogEntry[]>(`/logs${query ? '?' + query : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interconnect
|
||||||
|
export const getNodes = () => request<InterconnectNode[]>('/interconnect/nodes')
|
||||||
|
export const getNode = (id: string) => request<InterconnectNode>(`/interconnect/nodes/${id}`)
|
||||||
|
export const createNode = (data: Partial<InterconnectNode>) => request<InterconnectNode>('/interconnect/nodes', { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
export const updateNode = (id: string, data: Partial<InterconnectNode>) => request<InterconnectNode>(`/interconnect/nodes/${id}`, { method: 'PUT', body: JSON.stringify(data) })
|
||||||
|
export const deleteNode = (id: string) => request<void>(`/interconnect/nodes/${id}`, { method: 'DELETE' })
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
export const getSettings = () => request<Settings>('/settings')
|
||||||
|
export const updateSettings = (data: Partial<Settings>) => request<Settings>('/settings', { method: 'PUT', body: JSON.stringify(data) })
|
||||||
|
|
||||||
|
// Monitor
|
||||||
|
export const getMonitor = () => request<MonitorStats>('/monitor')
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import * as api from '@/api/endpoints'
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
export function useUser() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['user'],
|
||||||
|
queryFn: api.getCurrentUser,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
export function useMonitorStats() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['monitor'],
|
||||||
|
queryFn: api.getMonitor,
|
||||||
|
refetchInterval: 5000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTaskStats() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['taskStats'],
|
||||||
|
queryFn: api.getTaskStats,
|
||||||
|
refetchInterval: 10000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
export function useTasks(params?: { status?: string; tag?: string }) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['tasks', params],
|
||||||
|
queryFn: () => api.getTasks(params),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTask(id: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['task', id],
|
||||||
|
queryFn: () => api.getTask(id),
|
||||||
|
enabled: id > 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateTask() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createTask,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tasks'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateTask() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: number; data: Parameters<typeof api.updateTask>[1] }) =>
|
||||||
|
api.updateTask(id, data),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tasks'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteTask() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteTask,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['tasks'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useExecuteTask() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.executeTask,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['taskStats'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scripts
|
||||||
|
export function useScripts() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['scripts'],
|
||||||
|
queryFn: api.getScripts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScript(id: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['script', id],
|
||||||
|
queryFn: () => api.getScript(id),
|
||||||
|
enabled: id > 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateScript() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createScript,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['scripts'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteScript() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteScript,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['scripts'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
export function useLogs(params?: { task_id?: number; status?: string; page?: number }) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['logs', params],
|
||||||
|
queryFn: () => api.getLogs(params),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interconnect
|
||||||
|
export function useNodes() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['nodes'],
|
||||||
|
queryFn: api.getNodes,
|
||||||
|
refetchInterval: 30000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateNode() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createNode,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['nodes'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteNode() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteNode,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['nodes'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Env
|
||||||
|
export function useEnvs() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['envs'],
|
||||||
|
queryFn: api.getEnvs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateEnv() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createEnv,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['envs'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteEnv() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteEnv,
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['envs'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// API Types for TaskPool
|
||||||
|
|
||||||
|
export interface MonitorStats {
|
||||||
|
env: {
|
||||||
|
os: string
|
||||||
|
arch: string
|
||||||
|
go_version: string
|
||||||
|
num_cpu: number
|
||||||
|
goroutines: number
|
||||||
|
}
|
||||||
|
host: {
|
||||||
|
cpu_percent: number
|
||||||
|
mem_total: number
|
||||||
|
mem_used: number
|
||||||
|
mem_percent: number
|
||||||
|
disk_total: number
|
||||||
|
disk_used: number
|
||||||
|
disk_percent: number
|
||||||
|
uptime: number
|
||||||
|
platform: string
|
||||||
|
}
|
||||||
|
scheduler: {
|
||||||
|
scheduled: number
|
||||||
|
running: number
|
||||||
|
queue_size: number
|
||||||
|
worker_count: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
script: string
|
||||||
|
cron: string
|
||||||
|
status: string
|
||||||
|
tags: string[]
|
||||||
|
env_ids: number[]
|
||||||
|
language: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
last_run?: string
|
||||||
|
next_run?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Script {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
filename: string
|
||||||
|
content: string
|
||||||
|
language: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvVar {
|
||||||
|
id: number
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
tags: string[]
|
||||||
|
remark: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
id: number
|
||||||
|
task_id: number
|
||||||
|
task_name: string
|
||||||
|
status: string
|
||||||
|
output: string
|
||||||
|
started_at: string
|
||||||
|
finished_at: string
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InterconnectNode {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
token: string
|
||||||
|
remark: string
|
||||||
|
status: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
metrics?: {
|
||||||
|
cpu_percent: number
|
||||||
|
mem_percent: number
|
||||||
|
disk_percent: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
site_name: string
|
||||||
|
site_url: string
|
||||||
|
theme: string
|
||||||
|
language: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
+154
-41
@@ -1,68 +1,181 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useMonitorStats, useTaskStats } from '@/api/hooks'
|
||||||
import { Activity, Cpu, HardDrive, MemoryStick, Clock } from 'lucide-react'
|
import { Activity, Cpu, HardDrive, MemoryStick, Clock, Play, Pause, FileCode } from 'lucide-react'
|
||||||
import { request } from '@/api'
|
|
||||||
|
|
||||||
interface MonitorStats {
|
|
||||||
host: {
|
|
||||||
cpu_percent: number
|
|
||||||
mem_percent: number
|
|
||||||
disk_percent: number
|
|
||||||
uptime: number
|
|
||||||
}
|
|
||||||
scheduler: {
|
|
||||||
scheduled: number
|
|
||||||
running: number
|
|
||||||
queue_size: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { data: stats, isLoading } = useQuery({
|
const { data: stats, isLoading: statsLoading } = useMonitorStats()
|
||||||
queryKey: ['monitor'],
|
const { data: taskStats, isLoading: taskLoading } = useTaskStats()
|
||||||
queryFn: () => request<MonitorStats>('/monitor/stats'),
|
|
||||||
refetchInterval: 5000,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (statsLoading || taskLoading) {
|
||||||
return <div style={{ color: 'var(--text-muted)' }}>Loading...</div>
|
return <div style={{ color: 'var(--text-muted)', padding: 20 }}>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatUptime = (seconds: number) => {
|
const formatUptime = (seconds: number) => {
|
||||||
const days = Math.floor(seconds / 86400)
|
const days = Math.floor(seconds / 86400)
|
||||||
const hours = Math.floor((seconds % 86400) / 3600)
|
const hours = Math.floor((seconds % 86400) / 3600)
|
||||||
if (days > 0) return `${days}d ${hours}h`
|
if (days > 0) return `${days}d ${hours}h`
|
||||||
return `${hours}h`
|
return `${hours}h ${Math.floor((seconds % 3600) / 60)}m`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
const gb = bytes / (1024 * 1024 * 1024)
|
||||||
|
return `${gb.toFixed(1)} GB`
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Dashboard</h1>
|
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Dashboard</h1>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16 }}>
|
{/* System Stats */}
|
||||||
<Card icon={Cpu} label="CPU" value={`${stats?.host.cpu_percent || 0}%`} />
|
<section style={{ marginBottom: 32 }}>
|
||||||
<Card icon={MemoryStick} label="Memory" value={`${stats?.host.mem_percent || 0}%`} />
|
<h2 style={{ fontSize: 16, fontWeight: 500, marginBottom: 16, color: 'var(--text-secondary)' }}>System</h2>
|
||||||
<Card icon={HardDrive} label="Disk" value={`${stats?.host.disk_percent || 0}%`} />
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
|
||||||
<Card icon={Clock} label="Uptime" value={formatUptime(stats?.host.uptime || 0)} />
|
<StatCard
|
||||||
<Card icon={Activity} label="Scheduled" value={stats?.scheduler.scheduled || 0} />
|
icon={Cpu}
|
||||||
<Card icon={Activity} label="Running" value={stats?.scheduler.running || 0} />
|
label="CPU"
|
||||||
|
value={`${stats?.host.cpu_percent?.toFixed(1) || 0}%`}
|
||||||
|
color="#3b82f6"
|
||||||
|
progress={stats?.host.cpu_percent || 0}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={MemoryStick}
|
||||||
|
label="Memory"
|
||||||
|
value={`${stats?.host.mem_percent?.toFixed(1) || 0}%`}
|
||||||
|
subValue={`${formatBytes(stats?.host.mem_used || 0)} / ${formatBytes(stats?.host.mem_total || 0)}`}
|
||||||
|
color="#8b5cf6"
|
||||||
|
progress={stats?.host.mem_percent || 0}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={HardDrive}
|
||||||
|
label="Disk"
|
||||||
|
value={`${stats?.host.disk_percent?.toFixed(1) || 0}%`}
|
||||||
|
subValue={`${formatBytes(stats?.host.disk_used || 0)} / ${formatBytes(stats?.host.disk_total || 0)}`}
|
||||||
|
color="#06b6d4"
|
||||||
|
progress={stats?.host.disk_percent || 0}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={Clock}
|
||||||
|
label="Uptime"
|
||||||
|
value={formatUptime(stats?.host.uptime || 0)}
|
||||||
|
subValue={stats?.env.os || 'Unknown'}
|
||||||
|
color="#10b981"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Card({ icon: Icon, label, value }: { icon: any; label: string; value: string | number }) {
|
{/* Task Stats */}
|
||||||
return (
|
<section style={{ marginBottom: 32 }}>
|
||||||
|
<h2 style={{ fontSize: 16, fontWeight: 500, marginBottom: 16, color: 'var(--text-secondary)' }}>Tasks</h2>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
|
||||||
|
<StatCard
|
||||||
|
icon={FileCode}
|
||||||
|
label="Total Tasks"
|
||||||
|
value={taskStats?.total || 0}
|
||||||
|
color="#6366f1"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={Play}
|
||||||
|
label="Running"
|
||||||
|
value={taskStats?.running || 0}
|
||||||
|
color="#22c55e"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={Pause}
|
||||||
|
label="Scheduled"
|
||||||
|
value={taskStats?.scheduled || 0}
|
||||||
|
color="#f59e0b"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={Activity}
|
||||||
|
label="Workers"
|
||||||
|
value={stats?.scheduler.worker_count || 0}
|
||||||
|
color="#ec4899"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Runtime Info */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: 16, fontWeight: 500, marginBottom: 16, color: 'var(--text-secondary)' }}>Runtime</h2>
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'var(--bg-secondary)',
|
background: 'var(--bg-secondary)',
|
||||||
border: '1px solid var(--border)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 12 }}>
|
||||||
<Icon size={18} strokeWidth={1.5} style={{ color: 'var(--text-secondary)' }} />
|
<InfoRow label="OS" value={`${stats?.env.os} ${stats?.env.arch}`} />
|
||||||
<span style={{ color: 'var(--text-secondary)', fontSize: 13 }}>{label}</span>
|
<InfoRow label="Go Version" value={stats?.env.go_version || '-'} />
|
||||||
|
<InfoRow label="CPU Cores" value={stats?.env.num_cpu || '-'} />
|
||||||
|
<InfoRow label="Goroutines" value={stats?.env.goroutines || '-'} />
|
||||||
|
<InfoRow label="Platform" value={stats?.host.platform || '-'} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 24, fontWeight: 600 }}>{value}</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
subValue,
|
||||||
|
color,
|
||||||
|
progress
|
||||||
|
}: {
|
||||||
|
icon: any
|
||||||
|
label: string
|
||||||
|
value: string | number
|
||||||
|
subValue?: string
|
||||||
|
color: string
|
||||||
|
progress?: number
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
transition: 'border-color 0.15s',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: `${color}15`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<Icon size={16} strokeWidth={1.5} style={{ color }} />
|
||||||
|
</div>
|
||||||
|
<span style={{ color: 'var(--text-secondary)', fontSize: 13 }}>{label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ fontSize: 24, fontWeight: 600, marginBottom: 4 }}>{value}</div>
|
||||||
|
{subValue && <div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{subValue}</div>}
|
||||||
|
|
||||||
|
{progress !== undefined && (
|
||||||
|
<div style={{ marginTop: 12, height: 4, background: 'var(--bg-tertiary)', borderRadius: 2, overflow: 'hidden' }}>
|
||||||
|
<div style={{
|
||||||
|
height: '100%',
|
||||||
|
width: `${Math.min(progress, 100)}%`,
|
||||||
|
background: color,
|
||||||
|
borderRadius: 2,
|
||||||
|
transition: 'width 0.3s',
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ color: 'var(--text-secondary)', fontSize: 13 }}>{label}</span>
|
||||||
|
<span style={{ color: 'var(--text-primary)', fontSize: 13, fontWeight: 500 }}>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,180 @@
|
|||||||
|
import { useNodes, useDeleteNode } from '@/api/hooks'
|
||||||
|
import { Plus, Trash2, Server, RefreshCw, Wifi, WifiOff } from 'lucide-react'
|
||||||
|
|
||||||
export default function Interconnect() {
|
export default function Interconnect() {
|
||||||
|
const { data: nodes, isLoading, refetch } = useNodes()
|
||||||
|
const deleteNode = useDeleteNode()
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (confirm('Delete this node?')) {
|
||||||
|
await deleteNode.mutateAsync(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div style={{ color: 'var(--text-muted)', padding: 20 }}>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Interconnect</h1>
|
{/* Header */}
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>Node management coming soon...</p>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||||
|
<h1 style={{ fontSize: 24, fontWeight: 600 }}>Interconnect</h1>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={() => refetch()}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw size={14} strokeWidth={1.5} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-accent)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={14} strokeWidth={1.5} />
|
||||||
|
Add Node
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))', gap: 16 }}>
|
||||||
|
{nodes?.map(node => (
|
||||||
|
<NodeCard key={node.id} node={node} onDelete={() => handleDelete(node.id)} />
|
||||||
|
))}
|
||||||
|
{(!nodes || nodes.length === 0) && (
|
||||||
|
<div style={{
|
||||||
|
gridColumn: '1 / -1',
|
||||||
|
padding: 48,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
}}>
|
||||||
|
No nodes configured
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NodeCard({ node, onDelete }: { node: any; onDelete: () => void }) {
|
||||||
|
const isConnected = node.status === 'connected' || node.status === 'online'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: isConnected ? '#22c55e15' : '#ef444415',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<Server size={20} strokeWidth={1.5} style={{ color: isConnected ? '#22c55e' : '#ef4444' }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500, marginBottom: 2 }}>{node.name}</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{node.url}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onDelete}
|
||||||
|
title="Delete"
|
||||||
|
style={{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 4,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} strokeWidth={1.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||||
|
{isConnected ? (
|
||||||
|
<Wifi size={14} strokeWidth={1.5} style={{ color: '#22c55e' }} />
|
||||||
|
) : (
|
||||||
|
<WifiOff size={14} strokeWidth={1.5} style={{ color: '#ef4444' }} />
|
||||||
|
)}
|
||||||
|
<span style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: isConnected ? '#22c55e' : '#ef4444',
|
||||||
|
}}>
|
||||||
|
{isConnected ? 'Connected' : 'Disconnected'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metrics */}
|
||||||
|
{node.metrics && (
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||||
|
gap: 8,
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--bg-tertiary)',
|
||||||
|
borderRadius: 6,
|
||||||
|
}}>
|
||||||
|
<MetricItem label="CPU" value={`${node.metrics.cpu_percent?.toFixed(1) || 0}%`} />
|
||||||
|
<MetricItem label="Mem" value={`${node.metrics.mem_percent?.toFixed(1) || 0}%`} />
|
||||||
|
<MetricItem label="Disk" value={`${node.metrics.disk_percent?.toFixed(1) || 0}%`} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{node.remark && (
|
||||||
|
<div style={{ marginTop: 12, fontSize: 12, color: 'var(--text-muted)' }}>
|
||||||
|
{node.remark}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricItem({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginBottom: 2 }}>{label}</div>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 500 }}>{value}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { login } from '@/api/endpoints'
|
||||||
|
import { Lock, User, AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(username, password)
|
||||||
|
navigate({ to: '/' })
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Login failed')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
padding: 20,
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 400,
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 32,
|
||||||
|
}}>
|
||||||
|
{/* Logo */}
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
background: 'var(--bg-accent)',
|
||||||
|
borderRadius: 12,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto 16px',
|
||||||
|
}}>
|
||||||
|
<span style={{ color: 'white', fontSize: 24, fontWeight: 700 }}>T</span>
|
||||||
|
</div>
|
||||||
|
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 8 }}>TaskPool</h1>
|
||||||
|
<p style={{ color: 'var(--text-muted)', fontSize: 14 }}>Sign in to your account</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
padding: 12,
|
||||||
|
background: '#ef444415',
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 20,
|
||||||
|
}}>
|
||||||
|
<AlertCircle size={16} style={{ color: '#ef4444' }} />
|
||||||
|
<span style={{ color: '#ef4444', fontSize: 13 }}>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', fontWeight: 500, marginBottom: 8, fontSize: 13 }}>
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<User size={16} strokeWidth={1.5} style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="Enter username"
|
||||||
|
required
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px 10px 40px',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<label style={{ display: 'block', fontWeight: 500, marginBottom: 8, fontSize: 13 }}>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<Lock size={16} strokeWidth={1.5} style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Enter password"
|
||||||
|
required
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px 10px 40px',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--bg-accent)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: loading ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: loading ? 0.7 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Signing in...' : 'Sign In'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+136
-2
@@ -1,8 +1,142 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useLogs } from '@/api/hooks'
|
||||||
|
import { RefreshCw, CheckCircle, XCircle, Clock } from 'lucide-react'
|
||||||
|
|
||||||
export default function Logs() {
|
export default function Logs() {
|
||||||
|
const [filter, setFilter] = useState<{ status?: string }>({})
|
||||||
|
const { data: logs, isLoading, refetch } = useLogs(filter)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div style={{ color: 'var(--text-muted)', padding: 20 }}>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDuration = (ms: number) => {
|
||||||
|
if (ms < 1000) return `${ms}ms`
|
||||||
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
||||||
|
return `${(ms / 60000).toFixed(1)}m`
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Logs</h1>
|
{/* Header */}
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>Log viewer coming soon...</p>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||||
|
<h1 style={{ fontSize: 24, fontWeight: 600 }}>Logs</h1>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<select
|
||||||
|
value={filter.status || ''}
|
||||||
|
onChange={(e) => setFilter(f => ({ ...f, status: e.target.value || undefined }))}
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="success">Success</option>
|
||||||
|
<option value="failed">Failed</option>
|
||||||
|
<option value="running">Running</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={() => refetch()}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw size={14} strokeWidth={1.5} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Log List */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{logs?.map(log => (
|
||||||
|
<div
|
||||||
|
key={log.id}
|
||||||
|
style={{
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
{log.status === 'success' ? (
|
||||||
|
<CheckCircle size={18} strokeWidth={1.5} style={{ color: '#22c55e' }} />
|
||||||
|
) : log.status === 'failed' ? (
|
||||||
|
<XCircle size={18} strokeWidth={1.5} style={{ color: '#ef4444' }} />
|
||||||
|
) : (
|
||||||
|
<Clock size={18} strokeWidth={1.5} style={{ color: '#f59e0b' }} />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500 }}>{log.task_name}</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||||
|
{new Date(log.started_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 11,
|
||||||
|
background: log.status === 'success' ? '#22c55e15' : log.status === 'failed' ? '#ef444415' : '#f59e0b15',
|
||||||
|
color: log.status === 'success' ? '#22c55e' : log.status === 'failed' ? '#ef4444' : '#f59e0b',
|
||||||
|
}}>
|
||||||
|
{log.status}
|
||||||
|
</span>
|
||||||
|
{log.duration > 0 && (
|
||||||
|
<span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
||||||
|
{formatDuration(log.duration)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{log.output && (
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 12,
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
maxHeight: 200,
|
||||||
|
overflow: 'auto',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
}}>
|
||||||
|
{log.output.slice(0, 500)}
|
||||||
|
{log.output.length > 500 && '...'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!logs || logs.length === 0) && (
|
||||||
|
<div style={{
|
||||||
|
padding: 48,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
}}>
|
||||||
|
No logs found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+161
-2
@@ -1,8 +1,167 @@
|
|||||||
|
import { useScripts, useDeleteScript } from '@/api/hooks'
|
||||||
|
import { Plus, Trash2, FileCode, RefreshCw } from 'lucide-react'
|
||||||
|
import type { Script } from '@/api/types'
|
||||||
|
|
||||||
export default function Scripts() {
|
export default function Scripts() {
|
||||||
|
const { data: scripts, isLoading, refetch } = useScripts()
|
||||||
|
const deleteScript = useDeleteScript()
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
if (confirm('Delete this script?')) {
|
||||||
|
await deleteScript.mutateAsync(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div style={{ color: 'var(--text-muted)', padding: 20 }}>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Scripts</h1>
|
{/* Header */}
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>Script management coming soon...</p>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||||
|
<h1 style={{ fontSize: 24, fontWeight: 600 }}>Scripts</h1>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={() => refetch()}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw size={14} strokeWidth={1.5} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-accent)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={14} strokeWidth={1.5} />
|
||||||
|
Add Script
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 }}>
|
||||||
|
{scripts?.map(script => (
|
||||||
|
<ScriptCard key={script.id} script={script} onDelete={() => handleDelete(script.id)} />
|
||||||
|
))}
|
||||||
|
{(!scripts || scripts.length === 0) && (
|
||||||
|
<div style={{
|
||||||
|
gridColumn: '1 / -1',
|
||||||
|
padding: 48,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--text-muted)',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
}}>
|
||||||
|
No scripts found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScriptCard({ script, onDelete }: { script: Script; onDelete: () => void }) {
|
||||||
|
const formatDate = (date: string) => {
|
||||||
|
return new Date(date).toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLanguageColor = (lang: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
python: '#3776ab',
|
||||||
|
javascript: '#f7df1e',
|
||||||
|
typescript: '#3178c6',
|
||||||
|
go: '#00add8',
|
||||||
|
bash: '#4eaa25',
|
||||||
|
shell: '#4eaa25',
|
||||||
|
}
|
||||||
|
return colors[lang?.toLowerCase()] || '#6b7280'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
transition: 'border-color 0.15s',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: `${getLanguageColor(script.language)}15`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<FileCode size={20} strokeWidth={1.5} style={{ color: getLanguageColor(script.language) }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500, marginBottom: 2 }}>{script.name}</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', fontFamily: 'monospace' }}>{script.filename}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onDelete}
|
||||||
|
title="Delete"
|
||||||
|
style={{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 4,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} strokeWidth={1.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
background: `${getLanguageColor(script.language)}15`,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 11,
|
||||||
|
color: getLanguageColor(script.language),
|
||||||
|
fontWeight: 500,
|
||||||
|
}}>
|
||||||
|
{script.language || 'Unknown'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
||||||
|
Updated: {formatDate(script.updated_at)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+136
-2
@@ -1,8 +1,142 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useTheme } from '@/context/ThemeContext'
|
||||||
|
import { Save, RotateCcw } from 'lucide-react'
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const [siteName, setSiteName] = useState('TaskPool')
|
||||||
|
const [saved, setSaved] = useState(false)
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
// Save settings
|
||||||
|
setSaved(true)
|
||||||
|
setTimeout(() => setSaved(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setSiteName('TaskPool')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ maxWidth: 800 }}>
|
||||||
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Settings</h1>
|
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Settings</h1>
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>Settings coming soon...</p>
|
|
||||||
|
{/* Appearance */}
|
||||||
|
<section style={{ marginBottom: 32 }}>
|
||||||
|
<h2 style={{ fontSize: 16, fontWeight: 500, marginBottom: 16, color: 'var(--text-secondary)' }}>Appearance</h2>
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500, marginBottom: 4 }}>Theme</div>
|
||||||
|
<div style={{ fontSize: 13, color: 'var(--text-muted)' }}>Select your preferred color scheme</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setTheme('light')}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
background: theme === 'light' ? 'var(--bg-accent)' : 'var(--bg-tertiary)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: theme === 'light' ? 'white' : 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setTheme('dark')}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
background: theme === 'dark' ? 'var(--bg-accent)' : 'var(--bg-tertiary)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: theme === 'dark' ? 'white' : 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Site Settings */}
|
||||||
|
<section style={{ marginBottom: 32 }}>
|
||||||
|
<h2 style={{ fontSize: 16, fontWeight: 500, marginBottom: 16, color: 'var(--text-secondary)' }}>Site</h2>
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
}}>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', fontWeight: 500, marginBottom: 8 }}>Site Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={siteName}
|
||||||
|
onChange={(e) => setSiteName(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px',
|
||||||
|
background: 'var(--bg-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div style={{ display: 'flex', gap: 12 }}>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '10px 16px',
|
||||||
|
background: 'var(--bg-accent)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Save size={16} strokeWidth={1.5} />
|
||||||
|
{saved ? 'Saved!' : 'Save Changes'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '10px 16px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RotateCcw size={16} strokeWidth={1.5} />
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+228
-2
@@ -1,8 +1,234 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useTasks, useDeleteTask, useExecuteTask, useTags } from '@/api/hooks'
|
||||||
|
import { Play, Pause, Trash2, Plus, Filter, RefreshCw } from 'lucide-react'
|
||||||
|
import type { Task } from '@/api/types'
|
||||||
|
|
||||||
export default function Tasks() {
|
export default function Tasks() {
|
||||||
|
const [filter, setFilter] = useState<{ status?: string; tag?: string }>({})
|
||||||
|
const { data: tasks, isLoading, refetch } = useTasks(filter)
|
||||||
|
const { data: tags } = useTags()
|
||||||
|
const deleteTask = useDeleteTask()
|
||||||
|
const executeTask = useExecuteTask()
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
if (confirm('Delete this task?')) {
|
||||||
|
await deleteTask.mutateAsync(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExecute = async (id: number) => {
|
||||||
|
await executeTask.mutateAsync(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div style={{ color: 'var(--text-muted)', padding: 20 }}>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Tasks</h1>
|
{/* Header */}
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>Task management coming soon...</p>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||||
|
<h1 style={{ fontSize: 24, fontWeight: 600 }}>Tasks</h1>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={() => refetch()}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw size={14} strokeWidth={1.5} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-accent)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={14} strokeWidth={1.5} />
|
||||||
|
Add Task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
|
||||||
|
<select
|
||||||
|
value={filter.status || ''}
|
||||||
|
onChange={(e) => setFilter(f => ({ ...f, status: e.target.value || undefined }))}
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="enabled">Enabled</option>
|
||||||
|
<option value="disabled">Disabled</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<select
|
||||||
|
value={filter.tag || ''}
|
||||||
|
onChange={(e) => setFilter(f => ({ ...f, tag: e.target.value || undefined }))}
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">All Tags</option>
|
||||||
|
{tags.map(tag => <option key={tag} value={tag}>{tag}</option>)}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: 'var(--bg-tertiary)' }}>
|
||||||
|
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>Name</th>
|
||||||
|
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>Cron</th>
|
||||||
|
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>Status</th>
|
||||||
|
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>Language</th>
|
||||||
|
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>Tags</th>
|
||||||
|
<th style={{ padding: '12px 16px', textAlign: 'right', fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tasks?.map(task => (
|
||||||
|
<TaskRow
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
onDelete={() => handleDelete(task.id)}
|
||||||
|
onExecute={() => handleExecute(task.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{(!tasks || tasks.length === 0) && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} style={{ padding: 32, textAlign: 'center', color: 'var(--text-muted)' }}>
|
||||||
|
No tasks found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TaskRow({ task, onDelete, onExecute }: { task: Task; onDelete: () => void; onExecute: () => void }) {
|
||||||
|
return (
|
||||||
|
<tr style={{ borderTop: '1px solid var(--border)' }}>
|
||||||
|
<td style={{ padding: '12px 16px' }}>
|
||||||
|
<div style={{ fontWeight: 500 }}>{task.name}</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{task.script}</div>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 16px', fontSize: 13, color: 'var(--text-secondary)', fontFamily: 'monospace' }}>
|
||||||
|
{task.cron}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 16px' }}>
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
background: task.status === 'enabled' ? '#22c55e15' : '#f59e0b15',
|
||||||
|
color: task.status === 'enabled' ? '#22c55e' : '#f59e0b',
|
||||||
|
}}>
|
||||||
|
{task.status === 'enabled' ? 'Enabled' : 'Disabled'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 16px', fontSize: 13, color: 'var(--text-secondary)' }}>
|
||||||
|
{task.language || '-'}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 16px' }}>
|
||||||
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||||
|
{task.tags?.map(tag => (
|
||||||
|
<span key={tag} style={{
|
||||||
|
padding: '2px 6px',
|
||||||
|
background: 'var(--bg-tertiary)',
|
||||||
|
borderRadius: 3,
|
||||||
|
fontSize: 11,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
}}>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px 16px', textAlign: 'right' }}>
|
||||||
|
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
onClick={onExecute}
|
||||||
|
title="Run now"
|
||||||
|
style={{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 4,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play size={14} strokeWidth={1.5} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onDelete}
|
||||||
|
title="Delete"
|
||||||
|
style={{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 4,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} strokeWidth={1.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
+139
-3
@@ -1,8 +1,144 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { Terminal as XTerminal } from '@xterm/xterm'
|
||||||
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
|
import '@xterm/xterm/css/xterm.css'
|
||||||
|
import { Maximize2, Minimize2, RefreshCw } from 'lucide-react'
|
||||||
|
|
||||||
export default function Terminal() {
|
export default function Terminal() {
|
||||||
|
const terminalRef = useRef<HTMLDivElement>(null)
|
||||||
|
const xtermRef = useRef<XTerminal | null>(null)
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!terminalRef.current || xtermRef.current) return
|
||||||
|
|
||||||
|
const term = new XTerminal({
|
||||||
|
theme: {
|
||||||
|
background: '#0a0a0a',
|
||||||
|
foreground: '#fafafa',
|
||||||
|
cursor: '#3b82f6',
|
||||||
|
cursorAccent: '#0a0a0a',
|
||||||
|
selectionBackground: '#3b82f630',
|
||||||
|
},
|
||||||
|
fontFamily: 'JetBrains Mono, Menlo, monospace',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
cursorBlink: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fitAddon = new FitAddon()
|
||||||
|
term.loadAddon(fitAddon)
|
||||||
|
term.open(terminalRef.current)
|
||||||
|
fitAddon.fit()
|
||||||
|
|
||||||
|
term.writeln('\x1b[1;34mTaskPool Terminal\x1b[0m')
|
||||||
|
term.writeln('Type "help" for available commands')
|
||||||
|
term.writeln('')
|
||||||
|
term.write('$ ')
|
||||||
|
|
||||||
|
term.onKey(({ key, domEvent }) => {
|
||||||
|
const char = domEvent.key
|
||||||
|
|
||||||
|
if (char === 'Enter') {
|
||||||
|
term.writeln('')
|
||||||
|
term.write('$ ')
|
||||||
|
} else if (char === 'Backspace') {
|
||||||
|
term.write('\b \b')
|
||||||
|
} else {
|
||||||
|
term.write(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
xtermRef.current = term
|
||||||
|
|
||||||
|
const handleResize = () => fitAddon.fit()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
term.dispose()
|
||||||
|
xtermRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
setIsFullscreen(!isFullscreen)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (xtermRef.current) {
|
||||||
|
const fitAddon = new FitAddon()
|
||||||
|
xtermRef.current.loadAddon(fitAddon)
|
||||||
|
fitAddon.fit()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{
|
||||||
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 24 }}>Terminal</h1>
|
height: isFullscreen ? 'calc(100vh - 120px)' : 600,
|
||||||
<p style={{ color: 'var(--text-secondary)' }}>Web terminal coming soon...</p>
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
transition: 'height 0.2s',
|
||||||
|
}}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
}}>
|
||||||
|
<h1 style={{ fontSize: 24, fontWeight: 600 }}>Terminal</h1>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={() => xtermRef.current?.clear()}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw size={14} strokeWidth={1.5} />
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFullscreen ? <Minimize2 size={14} strokeWidth={1.5} /> : <Maximize2 size={14} strokeWidth={1.5} />}
|
||||||
|
{isFullscreen ? 'Exit' : 'Fullscreen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Terminal Container */}
|
||||||
|
<div
|
||||||
|
ref={terminalRef}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: '#0a0a0a',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+39
-18
@@ -1,67 +1,88 @@
|
|||||||
import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router'
|
import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router'
|
||||||
import Layout from '@/components/Layout'
|
import Layout from '@/components/Layout'
|
||||||
import Dashboard from '@/pages/Dashboard'
|
import Dashboard from '@/pages/Dashboard'
|
||||||
import Tasks from '@/pages/Tasks'
|
import Tasks from '@/pages/Tasks'
|
||||||
import Scripts from '@/pages/Scripts'
|
import Scripts from '@/pages/Scripts'
|
||||||
import Logs from '@/pages/Logs'
|
import Logs from '@/pages/Logs'
|
||||||
import Settings from '@/pages/Settings'
|
|
||||||
import Interconnect from '@/pages/Interconnect'
|
import Interconnect from '@/pages/Interconnect'
|
||||||
import Terminal from '@/pages/Terminal'
|
import Terminal from '@/pages/Terminal'
|
||||||
|
import Settings from '@/pages/Settings'
|
||||||
|
import Login from '@/pages/Login'
|
||||||
|
|
||||||
const rootRoute = createRootRoute({
|
const rootRoute = createRootRoute({
|
||||||
|
component: () => <Outlet />,
|
||||||
|
})
|
||||||
|
|
||||||
|
const loginRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/login',
|
||||||
|
component: Login,
|
||||||
|
})
|
||||||
|
|
||||||
|
const layoutRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
})
|
})
|
||||||
|
|
||||||
const indexRoute = createRoute({
|
const dashboardRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => layoutRoute,
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Dashboard,
|
component: Dashboard,
|
||||||
})
|
})
|
||||||
|
|
||||||
const tasksRoute = createRoute({
|
const tasksRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => layoutRoute,
|
||||||
path: '/tasks',
|
path: '/tasks',
|
||||||
component: Tasks,
|
component: Tasks,
|
||||||
})
|
})
|
||||||
|
|
||||||
const scriptsRoute = createRoute({
|
const scriptsRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => layoutRoute,
|
||||||
path: '/scripts',
|
path: '/scripts',
|
||||||
component: Scripts,
|
component: Scripts,
|
||||||
})
|
})
|
||||||
|
|
||||||
const logsRoute = createRoute({
|
const logsRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => layoutRoute,
|
||||||
path: '/logs',
|
path: '/logs',
|
||||||
component: Logs,
|
component: Logs,
|
||||||
})
|
})
|
||||||
|
|
||||||
const settingsRoute = createRoute({
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
path: '/settings',
|
|
||||||
component: Settings,
|
|
||||||
})
|
|
||||||
|
|
||||||
const interconnectRoute = createRoute({
|
const interconnectRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => layoutRoute,
|
||||||
path: '/interconnect',
|
path: '/interconnect',
|
||||||
component: Interconnect,
|
component: Interconnect,
|
||||||
})
|
})
|
||||||
|
|
||||||
const terminalRoute = createRoute({
|
const terminalRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => layoutRoute,
|
||||||
path: '/terminal',
|
path: '/terminal',
|
||||||
component: Terminal,
|
component: Terminal,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const routeTree = rootRoute.addChildren([
|
const settingsRoute = createRoute({
|
||||||
indexRoute,
|
getParentRoute: () => layoutRoute,
|
||||||
|
path: '/settings',
|
||||||
|
component: Settings,
|
||||||
|
})
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren([
|
||||||
|
loginRoute,
|
||||||
|
layoutRoute.addChildren([
|
||||||
|
dashboardRoute,
|
||||||
tasksRoute,
|
tasksRoute,
|
||||||
scriptsRoute,
|
scriptsRoute,
|
||||||
logsRoute,
|
logsRoute,
|
||||||
settingsRoute,
|
|
||||||
interconnectRoute,
|
interconnectRoute,
|
||||||
terminalRoute,
|
terminalRoute,
|
||||||
|
settingsRoute,
|
||||||
|
]),
|
||||||
])
|
])
|
||||||
|
|
||||||
export const router = createRouter({ routeTree })
|
export const router = createRouter({ routeTree })
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user