feat: Implement VMware console access with a new API endpoint and UI, and update the admin header to display user details.

This commit is contained in:
pbao9
2026-02-14 15:36:25 +07:00
parent 78b87c10fe
commit 449502e290
7 changed files with 433 additions and 452 deletions
@@ -193,9 +193,6 @@ class ProductController extends Controller
); );
return DataTables::eloquent($query) return DataTables::eloquent($query)
->addColumn('checkbox', function ($d) {
return '<input type="checkbox" class="form-check-input dt-checkbox" value="' . $d->id . '">';
})
->addColumn('category', function ($d) { ->addColumn('category', function ($d) {
return view('administrator.pages.product.datatable.category', ['row' => $d])->render(); return view('administrator.pages.product.datatable.category', ['row' => $d])->render();
}) })
@@ -208,13 +205,10 @@ class ProductController extends Controller
->addColumn('status', function ($d) { ->addColumn('status', function ($d) {
return view('administrator.pages.product.datatable.status', ['row' => $d])->render(); return view('administrator.pages.product.datatable.status', ['row' => $d])->render();
}) })
->addColumn( ->addColumn('action', function ($d) {
'action',
function ($d) {
return view('administrator.pages.product.datatable.action', ['row' => $d])->render(); return view('administrator.pages.product.datatable.action', ['row' => $d])->render();
} })
) ->rawColumns(['category', 'price', 'specs', 'status', 'action'])
->rawColumns(['checkbox', 'category', 'price', 'specs', 'status', 'action'])
->make(true); ->make(true);
} }
@@ -186,27 +186,31 @@
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="w-30px h-30px ms-1 rounded-circle" src="{{ url('https://placehold.co/30x30') }}" alt="profile"> <img class="w-30px h-30px ms-1 rounded-circle"
src="https://ui-avatars.com/api/?name={{ auth()->user()->lastname }}+{{ auth()->user()->firstname }}&background=random"
alt="profile">
</a> </a>
<div class="dropdown-menu p-0" aria-labelledby="profileDropdown"> <div class="dropdown-menu p-0" aria-labelledby="profileDropdown">
<div class="d-flex flex-column align-items-center border-bottom px-5 py-3"> <div class="d-flex flex-column align-items-center border-bottom px-5 py-3">
<div class="mb-3"> <div class="mb-3">
<img class="w-80px h-80px rounded-circle" src="{{ url('https://placehold.co/80x80') }}" alt=""> <img class="w-80px h-80px rounded-circle"
src="https://ui-avatars.com/api/?name={{ auth()->user()->lastname }}+{{ auth()->user()->firstname }}&background=random"
alt="">
</div> </div>
<div class="text-center"> <div class="text-center">
<p class="fs-16px fw-bolder">Amiah Burton</p> <p class="fs-16px fw-bolder">{{ auth()->user()->lastname }} {{ auth()->user()->firstname }}</p>
<p class="fs-12px text-secondary">amiahburton@gmail.com</p> <p class="fs-12px text-secondary">{{ auth()->user()->email }}</p>
</div> </div>
</div> </div>
<ul class="list-unstyled p-1"> <ul class="list-unstyled p-1">
<li> <li>
<a href="{{ url('/general/profile') }}" class="dropdown-item py-2 text-body ms-0"> <a href="{{ route('account') }}" class="dropdown-item py-2 text-body ms-0">
<i class="me-2 icon-md" data-lucide="user"></i> <i class="me-2 icon-md" data-lucide="user"></i>
<span>Profile</span> <span>Profile</span>
</a> </a>
</li> </li>
<li> <li>
<a href="javascript:;" class="dropdown-item py-2 text-body ms-0"> <a href="{{ route('account') }}" class="dropdown-item py-2 text-body ms-0">
<i class="me-2 icon-md" data-lucide="edit"></i> <i class="me-2 icon-md" data-lucide="edit"></i>
<span>Edit Profile</span> <span>Edit Profile</span>
</a> </a>
@@ -218,7 +222,7 @@
</a> </a>
</li> </li>
<li> <li>
<a href="javascript:;" class="dropdown-item py-2 text-body ms-0"> <a href="{{ route('logout') }}" class="dropdown-item py-2 text-body ms-0">
<i class="me-2 icon-md" data-lucide="log-out"></i> <i class="me-2 icon-md" data-lucide="log-out"></i>
<span>Log Out</span> <span>Log Out</span>
</a> </a>
@@ -1,11 +1,12 @@
<div class="dt-action-dropdown dropdown"> <div class="dt-action-dropdown dropdown">
<button class="btn btn-sm btn-light border d-flex align-items-center gap-1" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <button class="btn btn-sm btn-light d-flex align-items-center gap-1" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i data-lucide="more-vertical" width="14" height="14"></i> <i data-lucide="more-vertical" width="14" height="14"></i>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a class="dropdown-item" href="{{ route('products.edit', ['id' => $row->id]) }}"> <a class="dropdown-item" href="{{ route('products.edit', ['id' => $row->id]) }}">
<i data-lucide="pencil" width="14" height="14"></i> <i data-lucide="pencil" width="14" height="14" class="me-2"></i>
Edit Edit
</a> </a>
</li> </li>
@@ -13,22 +14,42 @@
<form action="{{ route('products.duplicate', ['id' => $row->id]) }}" method="POST" class="d-inline"> <form action="{{ route('products.duplicate', ['id' => $row->id]) }}" method="POST" class="d-inline">
@csrf @csrf
<button type="submit" class="dropdown-item"> <button type="submit" class="dropdown-item">
<i data-lucide="copy" width="14" height="14"></i> <i data-lucide="copy" width="14" height="14" class="me-2"></i>
Duplicate Duplicate
</button> </button>
</form> </form>
</li> </li>
<li><hr class="dropdown-divider"></li>
<li> <li>
<form action="{{ route('products.destroy', ['id' => $row->id]) }}" method="POST" class="d-inline" <hr class="dropdown-divider">
onsubmit="return confirm('Are you sure you want to delete this product?')"> <li>
@csrf <button type="button" class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
@method('DELETE') <i data-lucide="trash-2" width="14" height="14" class="me-2 text-danger"></i>
<button type="submit" class="dropdown-item text-danger">
<i data-lucide="trash-2" width="14" height="14"></i>
Delete Delete
</button> </button>
</form>
</li> </li>
</ul> </ul>
</div> </div>
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form action="{{ route('products.destroy', ['id' => $row->id]) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<input type="hidden" name="id" value="{{ $row->id }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Xoá sản phẩm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Bạn có chắc chắn muốn xoá sản phẩm này?
<strong>{{ $row->name }}</strong>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Huỷ</button>
<button type="submit" class="btn btn-danger" id="confirmDeleteBtn">Xoá</button>
</div>
</div>
</form>
</div>
</div>
@@ -20,15 +20,12 @@ $breadcrumbItems = [
@csrf @csrf
@method('PUT') @method('PUT')
<div class="row align-items-start"> <div class="row align-items-start">
{{-- Left Column: Basic Info & Pricing --}}
<div class="col-md-8"> <div class="col-md-8">
<div class="card card-primary card-outline"> <div class="card card-primary card-outline">
<div class="card-header"> <div class="card-header bg-white">
<h5 class="card-title m-0">Edit Product</h5> <h5 class="card-title m-0">Edit Product</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
{{-- Basic Information --}}
<div class="mb-4"> <div class="mb-4">
<h5 class="text-primary border-bottom mb-3 pb-2">Basic Information</h5> <h5 class="text-primary border-bottom mb-3 pb-2">Basic Information</h5>
@@ -36,8 +33,7 @@ $breadcrumbItems = [
<div class="col-md-12"> <div class="col-md-12">
<div class="form-group"> <div class="form-group">
<label for="name">Product Name <span class="text-danger">*</span></label> <label for="name">Product Name <span class="text-danger">*</span></label>
<input type="text" id="name" name="name" <input type="text" id="name" name="name" value="{{ old('name', $product->name) }}"
value="{{ old('name', $product->name) }}"
class="form-control" required placeholder="Enter product name" /> class="form-control" required placeholder="Enter product name" />
</div> </div>
</div> </div>
@@ -195,7 +191,7 @@ $breadcrumbItems = [
{{-- Right Column: Classification & Settings --}} {{-- Right Column: Classification & Settings --}}
<div class="col-md-4 sticky-top top-10 z-0"> <div class="col-md-4 sticky-top top-10 z-0">
<div class="card card-secondary card-outline mb-3"> <div class="card card-secondary card-outline mb-3">
<div class="card-header"> <div class="card-header bg-white">
<h5 class="card-title m-0">Classification</h5> <h5 class="card-title m-0">Classification</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -325,7 +321,7 @@ $breadcrumbItems = [
</div> </div>
<div class="card card-secondary card-outline"> <div class="card card-secondary card-outline">
<div class="card-header"> <div class="card-header bg-white">
<h5 class="card-title m-0">Inventory & Limits</h5> <h5 class="card-title m-0">Inventory & Limits</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -7,17 +7,17 @@
@section('breadcrumb') @section('breadcrumb')
@php @php
$breadcrumbItems = [ $breadcrumbItems = [
['label' => 'Product'], ['label' => 'Product'],
['label' => 'Product / Services'], ['label' => 'Product / Services'],
['label' => 'Product List', 'active' => true], ['label' => 'Product List', 'active' => true],
]; ];
@endphp @endphp
<x-admin-breadcrumb :items="$breadcrumbItems" /> <x-admin-breadcrumb :items="$breadcrumbItems" />
@endsection @endsection
@section('tab') @section('tab')
@php @php
$tabs = [ $tabs = [
[ [
'label' => 'Product', 'label' => 'Product',
'route' => 'products.index', 'route' => 'products.index',
@@ -28,26 +28,25 @@ $tabs = [
'route' => 'products.group.index', 'route' => 'products.group.index',
'icon' => 'server', 'icon' => 'server',
], ],
]; ];
@endphp @endphp
<x-admin-tab :items="$tabs" /> <x-admin-tab :items="$tabs" />
@endsection @endsection
@section('content') @section('content')
@if (auth()->guard('admins')->user()->can('manager product')) @if (auth()->guard('admins')->user()->can('manager product'))
<div class="card card-primary card-outline rounded-4 shadow-none"> <div class="card card-primary card-outline rounded-4 shadow-none">
<div class="card-header d-flex justify-content-end gap-2 rounded-top-4"> <div class="card-header bg-white d-flex justify-content-end gap-2 rounded-top-4">
@include('administrator.pages.block.product.btn') @include('administrator.pages.block.product.btn')
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
@php @php
$productColumns = [ $productColumns = [
['key' => 'checkbox', 'label' => '<input type="checkbox" class="form-check-input" id="dt-master-checkbox">', 'orderable' => false, 'searchable' => false, 'className' => 'text-center'],
['key' => 'name', 'label' => 'Product Name'], ['key' => 'name', 'label' => 'Product Name'],
['key' => 'category', 'label' => 'Category / Type', 'searchable' => false], ['key' => 'category', 'label' => 'Category / Type'],
['key' => 'price', 'label' => 'Price', 'orderable' => false, 'searchable' => false], ['key' => 'price', 'label' => 'Price'],
['key' => 'specs', 'label' => 'Specifications', 'orderable' => false, 'searchable' => false], ['key' => 'specs', 'label' => 'Specifications'],
['key' => 'status', 'label' => 'Status', 'searchable' => false], ['key' => 'status', 'label' => 'Status'],
['key' => 'action', 'label' => '', 'orderable' => false, 'searchable' => false, 'className' => 'text-center'], ['key' => 'action', 'label' => 'Action'],
]; ];
@endphp @endphp
<x-datatable id="productTable" ajax="{{ route('products.datatable') }}" :columns="$productColumns" /> <x-datatable id="productTable" ajax="{{ route('products.datatable') }}" :columns="$productColumns" />
@@ -346,73 +346,6 @@ $dtColumns = collect($columns)
height: 100%; height: 100%;
} }
.data-table-wrapper .table thead th {
border-color: #f1f5f9 !important;
font-family: 'Inter', sans-serif;
vertical-align: middle;
position: sticky;
top: 0;
z-index: 2;
background-color: #f8f9fa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.table thead th:last-child,
.table tbody td:last-child {
position: sticky;
right: 0;
background: #fff;
z-index: 1;
border-left: 1px solid #f1f5f9;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.02);
width: 70px;
min-width: 70px;
max-width: 70px;
}
.table thead th:last-child {
background: #f8f9fa;
z-index: 3;
}
.table thead th:first-child,
.table tbody td:first-child {
position: sticky;
left: 0;
background: #fff;
z-index: 1;
border-right: 1px solid #f1f5f9;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.02);
}
.table thead th:first-child {
background: #f8f9fa;
z-index: 3;
}
.table tbody tr {
position: relative;
z-index: 0;
}
.table tbody tr:hover {
z-index: 5;
}
.table tbody tr:hover td {
background-color: #f8fafc !important;
cursor: pointer;
}
.data-table-wrapper .table td:not(:last-child) {
border-right: 1px solid #f1f5f9;
}
.data-table-wrapper .table thead tr.filter-row th {
top: 45px;
z-index: 2;
}
.data-table-wrapper .input-group-text { .data-table-wrapper .input-group-text {
color: #94a3b8; color: #94a3b8;
} }
@@ -584,35 +517,5 @@ $dtColumns = collect($columns)
gap: 0.5rem; gap: 0.5rem;
margin: 0; margin: 0;
} }
.dt-action-dropdown .dropdown-menu {
min-width: 140px;
padding: 0.25rem 0;
font-size: 0.8125rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
z-index: 1050;
position: absolute;
}
.dt-action-dropdown .dropdown-item {
padding: 0.4rem 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.15s ease;
}
.dt-action-dropdown .dropdown-item:hover {
background-color: #f8fafc;
}
.dt-action-dropdown .dropdown-item.text-danger:hover {
background-color: #fef2f2;
}
.dt-action-dropdown .dropdown-divider {
margin: 0.25rem 0;
}
</style> </style>
@endpush @endpush
+64
View File
@@ -57,6 +57,70 @@ Route::prefix('v1')
return response()->json(['data' => $data]); return response()->json(['data' => $data]);
})->name('api.GetAllImage'); })->name('api.GetAllImage');
Route::get('get-ticket-console', function (Request $request, ServiceVCenter $serviceVCenter) {
$product_id = $request->get('product');
if (!$product_id) {
return response()->json(['error' => true, 'message' => "Please choose product"]);
}
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')->find($product_id);
if (!$product) {
return response()->json(['error' => true, 'message' => "Can't find product"]);
}
$vcenter = null;
if ($product->ipAddress && $product->ipAddress->host && $product->ipAddress->host->datacenter && $product->ipAddress->host->datacenter->vCenter) {
$vcenter = $product->ipAddress->host->datacenter->vCenter;
}
if (!$vcenter) {
return response()->json(['error' => true, 'message' => "Can't find vCenter information"]);
}
// Đăng nhập vCenter trước khi lấy ticket
if (!$serviceVCenter->loginServer($vcenter)) {
return response()->json([
'error' => true,
'message' => 'Không thể đăng nhập vào vCenter'
]);
}
$ConsoleTicket = $serviceVCenter->GetConsoleTickets($product->vmware_id);
// Nếu login hết hạn, thử login lại 1 lần nữa
if ($ConsoleTicket === 'Login_Again') {
if ($serviceVCenter->loginServer($vcenter)) {
$ConsoleTicket = $serviceVCenter->GetConsoleTickets($product->vmware_id);
} else {
return response()->json([
'error' => true,
'message' => 'Không thể đăng nhập lại vào vCenter'
]);
}
}
// Nếu vẫn lỗi, trả về lỗi rõ ràng
if (is_string($ConsoleTicket)) {
return response()->json([
'error' => true,
'message' => "Failed to get console ticket: $ConsoleTicket"
]);
}
if ($ConsoleTicket['error']) {
return response()->json([
'error' => true,
'message' => $ConsoleTicket['data']
]);
}
return response()->json([
'error' => false,
'data' => $ConsoleTicket['data'],
'host' => $vcenter->host,
'vmware-api-session-id' => $serviceVCenter->token
])->cookie('vmware-api-session-id', $serviceVCenter->token, 60, '/', null, false, false);
})->name('api.GetConsoleTicket');
}); });
}); });
Route::any('{any}', function () { Route::any('{any}', function () {