up
This commit is contained in:
@@ -103,7 +103,7 @@
|
||||
<div class="space-y-4">
|
||||
<div v-for="service in systemServices" :key="service.name" class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div :class="['w-2 h-2 rounded-full', service.status === 'healthy' ? 'bg-green-500' : 'bg-red-500']"></div>
|
||||
<div :class="['w-2 h-2 rounded-full', service.status === 'up' ? 'bg-green-500' : 'bg-red-500']"></div>
|
||||
<span class="text-gray-700">{{ service.name }}</span>
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">{{ service.latency }}ms</span>
|
||||
@@ -115,7 +115,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import AppLayout from '../components/Layout/AppLayout.vue'
|
||||
|
||||
const stats = ref({
|
||||
@@ -126,12 +126,34 @@ const stats = ref({
|
||||
})
|
||||
|
||||
const recentUsers = ref([])
|
||||
const systemServices = ref([
|
||||
{ name: 'Database', status: 'healthy', latency: 12 },
|
||||
{ name: 'Redis Cache', status: 'healthy', latency: 3 },
|
||||
{ name: 'API Gateway', status: 'healthy', latency: 45 },
|
||||
{ name: 'File Storage', status: 'healthy', latency: 28 }
|
||||
])
|
||||
const systemServices = ref([])
|
||||
|
||||
async function loadHealth() {
|
||||
try {
|
||||
const res = await fetch('/api/health')
|
||||
const data = await res.json()
|
||||
if (data.checks) {
|
||||
systemServices.value = data.checks.map(check => ({
|
||||
name: check.data?.name || check.id,
|
||||
status: check.status.toLowerCase(),
|
||||
latency: check.data?.latency_ms || 0
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Health check failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
let interval: number
|
||||
onMounted(async () => {
|
||||
await loadData()
|
||||
await loadHealth()
|
||||
interval = window.setInterval(loadHealth, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (interval) clearInterval(interval)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadData()
|
||||
|
||||
140
frontend/src/views/Restaurants.vue
Normal file
140
frontend/src/views/Restaurants.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<AppLayout>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Restaurants</h1>
|
||||
<button @click="openModal('create')" class="btn-primary">+ Add Restaurant</button>
|
||||
</div>
|
||||
|
||||
<div class="card overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Login</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Host</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="rest in restaurants" :key="rest.id">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ rest.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ rest.name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.login }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.host }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(rest.created) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
|
||||
<button @click="openModal('edit', rest)" class="text-blue-600 hover:text-blue-800">Edit</button>
|
||||
<button @click="deleteRestaurant(rest.id)" class="text-red-600 hover:text-red-800">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div v-if="modalOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h2 class="text-xl font-bold mb-4">{{ modalTitle }}</h2>
|
||||
<form @submit.prevent="submitRestaurant">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input v-model="form.name" type="text" required class="input-field mt-1" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Login</label>
|
||||
<input v-model="form.login" type="text" required class="input-field mt-1" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input v-model="form.password" :required="modalMode === 'create'" type="password" class="input-field mt-1" />
|
||||
<p v-if="modalMode === 'edit'" class="text-xs text-gray-500 mt-1">Leave blank to keep current password</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Host</label>
|
||||
<input v-model="form.host" type="text" required class="input-field mt-1" />
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button type="button" @click="closeModal" class="btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import AppLayout from '../components/Layout/AppLayout.vue';
|
||||
|
||||
const restaurants = ref([]);
|
||||
const modalOpen = ref(false);
|
||||
const modalMode = ref<'create' | 'edit'>('create');
|
||||
const form = ref({ id: null, name: '', login: '', password: '', host: '' });
|
||||
const modalTitle = ref('');
|
||||
|
||||
async function loadRestaurants() {
|
||||
const res = await fetch('/api/admin/restaurants');
|
||||
restaurants.value = await res.json();
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
|
||||
function openModal(mode: 'create' | 'edit', rest: any = null) {
|
||||
modalMode.value = mode;
|
||||
if (mode === 'create') {
|
||||
form.value = { id: null, name: '', login: '', password: '', host: '' };
|
||||
modalTitle.value = 'Create Restaurant';
|
||||
} else {
|
||||
form.value = { id: rest.id, name: rest.name, login: rest.login, password: '', host: rest.host };
|
||||
modalTitle.value = 'Edit Restaurant';
|
||||
}
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
async function submitRestaurant() {
|
||||
try {
|
||||
const payload = {
|
||||
name: form.value.name,
|
||||
login: form.value.login,
|
||||
host: form.value.host,
|
||||
...(form.value.password ? { password: form.value.password } : {})
|
||||
};
|
||||
if (modalMode.value === 'create') {
|
||||
await fetch('/api/admin/restaurants', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
} else {
|
||||
await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
await loadRestaurants();
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
alert('Operation failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRestaurant(id: number) {
|
||||
if (confirm('Are you sure?')) {
|
||||
await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
|
||||
await loadRestaurants();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadRestaurants);
|
||||
</script>
|
||||
124
frontend/src/views/Users.vue
Normal file
124
frontend/src/views/Users.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<AppLayout>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Users Management</h1>
|
||||
<button @click="openModal('create')" class="btn-primary">+ Add User</button>
|
||||
</div>
|
||||
|
||||
<div class="card overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Login</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.login }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ user.ip || '-' }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(user.created) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
|
||||
<button @click="openModal('edit', user)" class="text-blue-600 hover:text-blue-800">Edit</button>
|
||||
<button @click="deleteUser(user.id)" class="text-red-600 hover:text-red-800">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div v-if="modalOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h2 class="text-xl font-bold mb-4">{{ modalTitle }}</h2>
|
||||
<form @submit.prevent="submitUser">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Login</label>
|
||||
<input v-model="form.login" type="text" required class="input-field mt-1" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input v-model="form.password" :required="modalMode === 'create'" type="password" class="input-field mt-1" />
|
||||
<p v-if="modalMode === 'edit'" class="text-xs text-gray-500 mt-1">Leave blank to keep current password</p>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button type="button" @click="closeModal" class="btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import AppLayout from '../components/Layout/AppLayout.vue';
|
||||
|
||||
const users = ref([]);
|
||||
const modalOpen = ref(false);
|
||||
const modalMode = ref<'create' | 'edit'>('create');
|
||||
const form = ref({ id: null, login: '', password: '' });
|
||||
const modalTitle = ref('');
|
||||
|
||||
async function loadUsers() {
|
||||
const res = await fetch('/api/admin/users');
|
||||
users.value = await res.json();
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
|
||||
function openModal(mode: 'create' | 'edit', user: any = null) {
|
||||
modalMode.value = mode;
|
||||
if (mode === 'create') {
|
||||
form.value = { id: null, login: '', password: '' };
|
||||
modalTitle.value = 'Create User';
|
||||
} else {
|
||||
form.value = { id: user.id, login: user.login, password: '' };
|
||||
modalTitle.value = 'Edit User';
|
||||
}
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
async function submitUser() {
|
||||
try {
|
||||
if (modalMode.value === 'create') {
|
||||
await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ login: form.value.login, password: form.value.password })
|
||||
});
|
||||
} else {
|
||||
await fetch(`/api/admin/users/${form.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ login: form.value.login, password: form.value.password || undefined })
|
||||
});
|
||||
}
|
||||
await loadUsers();
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
alert('Operation failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(id: number) {
|
||||
if (confirm('Are you sure?')) {
|
||||
await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
|
||||
await loadUsers();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadUsers);
|
||||
</script>
|
||||
Reference in New Issue
Block a user