Chuyển tới nội dung chính

Edge Storage Management UI

Tổng Quan​

Trang Edge Storage Management cung cấp giao diện quản lý các edge storage nodes trong hệ thống.

Vị Trí​

Route: /nodes
Component: frontend/pages/nodes.vue
Layout: default

Menu item "Edge Storage" được thêm vào sidebar navigation:

<NuxtLink to="/nodes">
<HardDrive class="w-5 h-5" />
<span>Edge Storage</span>
</NuxtLink>

Position: Sau System Control, trước Footer
Icon: HardDrive (lucide-vue-next)
Active State: Primary color với border khi active

UI Components​

1. Page Header​

<div class="flex items-center justify-between">
<div>
<h1>Edge Node Management</h1>
<p>Manage edge storage nodes</p>
</div>
<UiButton @click="showAddDialog = true">
<Plus class="w-4 h-4" />
<span>Add Node</span>
</UiButton>
</div>

Features:

  • Title và subtitle
  • "Add Node" button ở góc phải

2. Data Table​

Table hiển thị danh sách edge nodes với các cột:

ColumnTypeDescription
NameTextDisplay name của node
HostText (monospace)Hostname của RabbitMQ
PortNumberPort number
StatusBadgeOnline (green) / Offline (gray)
EnabledToggle SwitchEnable/disable node
ActionsIcon ButtonDelete button (trash icon)

Styling:

  • Glass card effect với border
  • Hover effect trên rows
  • Border between rows
  • Responsive table layout

3. Loading State​

<div v-if="loading">
<div class="animate-spin rounded-full h-12 w-12 border-b-2" />
<p>Loading nodes...</p>
</div>

When Shown:

  • Initial page load
  • After CRUD operations (briefly)

4. Empty State​

<div v-if="nodes.length === 0">
<Database class="w-16 h-16" />
<h3>No Edge Nodes</h3>
<p>Add your first edge node to get started</p>
</div>

When Shown:

  • Khi chưa có node nào trong hệ thống

5. Add Node Dialog​

Modal dialog để thêm edge node mới.

Form Fields:

<form @submit.prevent="addNode">
<input v-model="newNode.name" required placeholder="Subnet-CauGiay" />
<input v-model="newNode.host" required placeholder="rabbit-edge-1" />
<input v-model.number="newNode.port" type="number" required placeholder="5672" />
<input v-model="newNode.queueName" placeholder="city-data-queue-1 (optional)" />
<input v-model="newNode.username" placeholder="edge_user (optional)" />
<input v-model="newNode.password" type="password" placeholder="edge_pass (optional)" />
</form>

Validation:

  • Name: Required, string
  • Host: Required, string
  • Port: Required, number (default: 5672)
  • QueueName: Optional, string
  • Username: Optional, string
  • Password: Optional, string (masked)

Actions:

  • "Add Node" button (primary)
  • "Cancel" button (ghost variant)

Behavior:

  • Form reset sau khi submit thành công
  • Dialog đóng tự động
  • List refresh để hiển thị node mới
  • Error alert nếu tên đã tồn tại

6. Delete Confirmation Dialog​

Modal dialog xác nhận trước khi xóa.

<div v-if="deleteConfirm">
<h2>Delete Node</h2>
<p>
Are you sure you want to delete
<span class="font-semibold">{{ deleteConfirm }}</span>?
This action cannot be undone.
</p>
<UiButton @click="deleteNode">Delete</UiButton>
<UiButton @click="deleteConfirm = null">Cancel</UiButton>
</div>

Features:

  • Hiển thị tên node sẽ bị xóa
  • Warning message rõ ràng
  • Red "Delete" button
  • "Cancel" button để hủy

State Management​

Component sử dụng Vue Composition API với refs:

const nodes = ref<any[]>([]);           // Danh sách nodes
const loading = ref(true); // Loading state
const showAddDialog = ref(false); // Add dialog visibility
const deleteConfirm = ref<string | null>(null); // Node name to delete
const newNode = ref({ // Form data
name: '',
host: '',
port: 5672,
queueName: '',
username: '',
password: '',
});

API Integration​

Fetch Nodes​

const fetchNodes = async () => {
loading.value = true;
const data = await $fetch('/api/nodes');
nodes.value = data;
loading.value = false;
};

Endpoint: GET /api/nodes
When Called: onMounted, after CRUD operations

Add Node​

const addNode = async () => {
await $fetch('/api/nodes', {
method: 'POST',
body: newNode.value,
});
// Reset form
newNode.value = { name: '', host: '', port: 5672, queueName: '', username: '', password: '' };
showAddDialog.value = false;
await fetchNodes();
};

Endpoint: POST /api/nodes
Error Handling: Alert với error message

Toggle Node​

const toggleNode = async (name: string) => {
await $fetch(`/api/nodes/${'${name}'}/toggle`, {
method: 'PUT',
});
await fetchNodes();
};

Endpoint: PUT /api/nodes/{name}/toggle
UI Update: Optimistic - switch changes immediately, then confirms with fetch

Delete Node​

const deleteNode = async () => {
if (!deleteConfirm.value) return;
await $fetch(`/api/nodes/${'${deleteConfirm.value}'}`, {
method: 'DELETE',
});
deleteConfirm.value = null;
await fetchNodes();
};

Endpoint: DELETE /api/nodes/{name}
Confirmation: Required via dialog

Server API Routes​

Nuxt server routes proxy requests đến backend:

GET Route​

File: server/api/nodes/index.get.ts

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
const backendUrl = config.public.apiBase || "http://localhost:8080";
const nodes = await $fetch(`${'${backendUrl}'}/api/nodes`);
return nodes;

POST Route​

File: server/api/nodes/index.post.ts

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
const backendUrl = config.public.apiBase || "http://localhost:8080";
const body = await readBody(event);
const response = await $fetch(`${'${backendUrl}'}/api/nodes`, {
method: "POST",
body,
});
return response;
});

Toggle Route​

File: server/api/nodes/[name]/toggle.put.ts

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
const backendUrl = config.public.apiBase || "http://localhost:8080";
const name = getRouterParam(event, "name");
const response = await $fetch(`${'${backendUrl}'}/api/nodes/${'${name}'}/toggle`, {
method: "PUT",
});
return response;
});

Delete Route​

File: server/api/nodes/[name]/index.delete.ts

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
const backendUrl = config.public.apiBase || "http://localhost:8080";
const name = getRouterParam(event, "name");
const response = await $fetch(`${'${backendUrl}'}/api/nodes/${'${name}'}`, {
method: "DELETE",
});
return response;
});

Purpose:

  • Centralized backend URL configuration
  • CORS handling
  • Error transformation

Styling​

Design System​

  • Glass Card: glass-card class với backdrop blur
  • Primary Color: Blue gradient
  • Text Colors:
    • Headers: text-gray-100
    • Body: text-gray-400
    • Accents: text-primary
  • Borders: border-dark-border
  • Backgrounds: bg-dark-lighter

Status Colors​

Online: bg-green-500/20 text-green-400 border-green-500/40
Offline: bg-gray-500/20 text-gray-400 border-gray-500/40

Toggle Switch​

<button class="inline-flex h-6 w-11 rounded-full"
:class="enabled ? 'bg-primary' : 'bg-gray-600'">
<span class="h-4 w-4 rounded-full bg-white"
:class="enabled ? 'translate-x-6' : 'translate-x-1'">
</span>
</button>

States:

  • ON: Primary color, dot moved right
  • OFF: Gray color, dot moved left
  • Transition: Smooth animation

Responsive Design​

  • Desktop (≥1024px): Full table layout
  • Tablet (768px-1023px): Scrollable table
  • Mobile (<768px): Should implement card layout (future enhancement)

User Flow​

View Nodes​

  1. Click "Edge Storage" trong sidebar
  2. Page loads, shows loading state
  3. Danh sách nodes hiển thị trong table
  4. User có thể scroll nếu nhiều nodes

Add Node​

  1. Click "Add Node" button
  2. Dialog mở với empty form
  3. User nhập thông tin
  4. Click "Add Node"
  5. Validation check
  6. API call
  7. Success: Dialog đóng, list refresh
  8. Error: Alert hiển thị

Toggle Node​

  1. User click toggle switch
  2. Switch animation plays
  3. API call
  4. Success: Status badge updates
  5. Error: Switch reverts

Delete Node​

  1. Click trash icon
  2. Confirmation dialog hiển thị
  3. User confirms
  4. API call
  5. Success: Node removed from list
  6. Error: Dialog đóng, error shown

Error Handling​

Network Errors​

try {
await $fetch('/api/nodes');
} catch (error) {
console.error('Error fetching nodes:', error);
// Show error state in UI
}

Validation Errors​

catch (error: any) {
alert(error.data?.error || 'Failed to add node');
}

Best Practices​

  • Log errors to console
  • Show user-friendly messages
  • Don't expose technical details
  • Provide actionable guidance

Accessibility​

Keyboard Navigation​

  • Tab through interactive elements
  • Enter to submit forms
  • Escape to close dialogs

ARIA Labels​

<button aria-label="Delete node">
<Trash2 />
</button>

Screen Readers​

  • Semantic HTML elements
  • Proper heading hierarchy
  • Alt text cho icons

Performance​

Optimization Techniques​

  • Lazy load icons với dynamic imports
  • Debounce search/filter (future)
  • Virtual scrolling for large lists (future)
  • Optimize API calls (no unnecessary fetches)

Loading States​

  • Skeleton screens (future enhancement)
  • Spinner during data fetch
  • Disabled buttons during operations

Testing​

Component Tests​

  • [❌] Mount component with mock data
  • [❌] Test form validation
  • [❌] Test CRUD operations
  • [❌] Test error handling

E2E Tests​

  • [✅] Manual testing completed
  • [❌] Automated Playwright tests

Future Enhancements​

  • Search/filter functionality
  • Sort table columns
  • Bulk operations
  • Export node list
  • Import nodes từ CSV/JSON
  • Real-time status updates
  • Performance metrics per node