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:
@@ -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">
|
||||||
|
|||||||
@@ -35,19 +35,18 @@ $tabs = [
|
|||||||
@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
|
||||||
@@ -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 () {
|
||||||
|
|||||||
Reference in New Issue
Block a user