Merge pull request #1915 from pterodactyl/feature/server-mounts

Add configurable server mounts
This commit is contained in:
Dane Everitt 2020-07-11 17:19:25 -07:00 committed by GitHub
commit 0d35ab95fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1383 additions and 10 deletions

View file

@ -0,0 +1,149 @@
{{-- Pterodactyl - Panel --}}
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- This software is licensed under the terms of the MIT license. --}}
{{-- https://opensource.org/licenses/MIT --}}
@extends('layouts.admin')
@section('title')
Mounts
@endsection
@section('content-header')
<h1>Mounts<small>SoonTM</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Mounts</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Mount List</h3>
<div class="box-tools">
<button class="btn btn-sm btn-primary" data-toggle="modal" data-target="#newMountModal">Create New</button>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tbody>
<tr>
<th>ID</th>
<th>Name</th>
<th>Source</th>
<th>Target</th>
<th class="text-center">Eggs</th>
<th class="text-center">Nodes</th>
<th class="text-center">Servers</th>
</tr>
@foreach ($mounts as $mount)
<tr>
<td><code>{{ $mount->id }}</code></td>
<td><a href="{{ route('admin.mounts.view', $mount->id) }}">{{ $mount->name }}</a></td>
<td><code>{{ $mount->source }}</code></td>
<td><code>{{ $mount->target }}</code></td>
<td class="text-center">{{ $mount->eggs_count }}</td>
<td class="text-center">{{ $mount->nodes_count }}</td>
<td class="text-center">{{ $mount->servers_count }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal fade" id="newMountModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="{{ route('admin.mounts') }}" method="POST">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true" style="color: #FFFFFF">&times;</span>
</button>
<h4 class="modal-title">Create Mount</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<label for="pName" class="form-label">Name</label>
<input type="text" id="pName" name="name" class="form-control" />
<p class="text-muted small">Unique name used to separate this mount from another.</p>
</div>
<div class="col-md-12">
<label for="pDescription" class="form-label">Description</label>
<textarea id="pDescription" name="description" class="form-control" rows="4"></textarea>
<p class="text-muted small">A longer description for this mount, must be less than 255 characters.</p>
</div>
<div class="col-md-6">
<label for="pSource" class="form-label">Source</label>
<input type="text" id="pSource" name="source" class="form-control" />
<p class="text-muted small">File path on the host system to mount to a container.</p>
</div>
<div class="col-md-6">
<label for="pTarget" class="form-label">Target</label>
<input type="text" id="pTarget" name="target" class="form-control" />
<p class="text-muted small">Where the mount will be accessible inside a container.</p>
</div>
<div class="col-md-6">
<label class="form-label">Read Only</label>
<div>
<div class="radio radio-success radio-inline">
<input type="radio" id="pReadOnlyFalse" name="read_only" value="0" checked>
<label for="pReadOnlyFalse">False</label>
</div>
<div class="radio radio-warning radio-inline">
<input type="radio" id="pReadOnly" name="read_only" value="1">
<label for="pReadOnly">True</label>
</div>
</div>
<p class="text-muted small">Is the mount read only inside the container?</p>
</div>
<div class="col-md-6">
<label class="form-label">User Mountable</label>
<div>
<div class="radio radio-success radio-inline">
<input type="radio" id="pUserMountableFalse" name="user_mountable" value="0" checked>
<label for="pUserMountableFalse">False</label>
</div>
<div class="radio radio-warning radio-inline">
<input type="radio" id="pUserMountable" name="user_mountable" value="1">
<label for="pUserMountable">True</label>
</div>
</div>
<p class="text-muted small">Should users be able to mount this themselves?</p>
</div>
</div>
</div>
<div class="modal-footer">
{!! csrf_field() !!}
<button type="button" class="btn btn-default btn-sm pull-left" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success btn-sm">Create</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,319 @@
{{-- Pterodactyl - Panel --}}
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- This software is licensed under the terms of the MIT license. --}}
{{-- https://opensource.org/licenses/MIT --}}
@extends('layouts.admin')
@section('title')
Mounts &rarr; View &rarr; {{ $mount->id }}
@endsection
@section('content-header')
<h1>{{ $mount->name }}<small>{{ str_limit($mount->description, 75) }}</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.mounts') }}">Mounts</a></li>
<li class="active">{{ $mount->name }}</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-sm-6">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Mount Details</h3>
</div>
<form action="{{ route('admin.mounts.view', $mount->id) }}" method="POST">
<div class="box-body">
<div class="form-group">
<label for="PUniqueID" class="form-label">Unique ID</label>
<input type="text" id="PUniqueID" class="form-control" value="{{ $mount->uuid }}" disabled />
</div>
<div class="form-group">
<label for="pName" class="form-label">Name</label>
<input type="text" id="pName" name="name" class="form-control" value="{{ $mount->name }}" />
</div>
<div class="form-group">
<label for="pDescription" class="form-label">Description</label>
<textarea id="pDescription" name="description" class="form-control" rows="4">{{ $mount->description }}</textarea>
</div>
<div class="row">
<div class="form-group col-md-6">
<label for="pSource" class="form-label">Source</label>
<input type="text" id="pSource" name="source" class="form-control" value="{{ $mount->source }}" />
</div>
<div class="form-group col-md-6">
<label for="pTarget" class="form-label">Target</label>
<input type="text" id="pTarget" name="target" class="form-control" value="{{ $mount->target }}" />
</div>
</div>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label">Read Only</label>
<div>
<div class="radio radio-success radio-inline">
<input type="radio" id="pReadOnlyFalse" name="read_only" value="0" @if(!$mount->read_only) checked @endif>
<label for="pReadOnlyFalse">False</label>
</div>
<div class="radio radio-warning radio-inline">
<input type="radio" id="pReadOnly" name="read_only" value="1" @if($mount->read_only) checked @endif>
<label for="pReadOnly">True</label>
</div>
</div>
</div>
<div class="form-group col-md-6">
<label class="form-label">User Mountable</label>
<div>
<div class="radio radio-success radio-inline">
<input type="radio" id="pUserMountableFalse" name="user_mountable" value="0" @if(!$mount->user_mountable) checked @endif>
<label for="pUserMountableFalse">False</label>
</div>
<div class="radio radio-warning radio-inline">
<input type="radio" id="pUserMountable" name="user_mountable" value="1" @if($mount->user_mountable) checked @endif>
<label for="pUserMountable">True</label>
</div>
</div>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
{!! method_field('PATCH') !!}
<button name="action" value="edit" class="btn btn-sm btn-primary pull-right">Save</button>
<button name="action" value="delete" class="btn btn-sm btn-danger pull-left muted muted-hover"><i class="fa fa-trash-o"></i></button>
</div>
</form>
</div>
</div>
<div class="col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Eggs</h3>
<div class="box-tools">
<button class="btn btn-sm btn-primary" data-toggle="modal" data-target="#addEggsModal">Add Eggs</button>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<th>ID</th>
<th>Name</th>
<th></th>
</tr>
@foreach ($mount->eggs as $egg)
<tr>
<td class="col-sm-2 middle"><code>{{ $egg->id }}</code></td>
<td class="middle"><a href="{{ route('admin.nests.egg.view', $egg->id) }}">{{ $egg->name }}</a></td>
<td class="col-sm-1 middle">
<button data-action="detach-egg" data-id="{{ $egg->id }}" class="btn btn-sm btn-danger"><i class="fa fa-trash-o"></i></button>
</td>
</tr>
@endforeach
</table>
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Nodes</h3>
<div class="box-tools">
<button class="btn btn-sm btn-primary" data-toggle="modal" data-target="#addNodesModal">Add Nodes</button>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<th>ID</th>
<th>Name</th>
<th>FQDN</th>
<th></th>
</tr>
@foreach ($mount->nodes as $node)
<tr>
<td class="col-sm-2 middle"><code>{{ $node->id }}</code></td>
<td class="middle"><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td>
<td class="middle"><code>{{ $node->fqdn }}</code></td>
<td class="col-sm-1 middle">
<button data-action="detach-node" data-id="{{ $node->id }}" class="btn btn-sm btn-danger"><i class="fa fa-trash-o"></i></button>
</td>
</tr>
@endforeach
</table>
</div>
</div>
</div>
</div>
<div class="modal fade" id="addEggsModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="{{ route('admin.mounts.eggs', $mount->id) }}" method="POST">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true" style="color: #FFFFFF">&times;</span>
</button>
<h4 class="modal-title">Add Eggs</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="form-group col-md-12">
<label for="pEggs">Eggs</label>
<select id="pEggs" name="eggs[]" class="form-control" multiple>
@foreach ($nests as $nest)
<optgroup label="{{ $nest->name }}">
@foreach ($nest->eggs as $egg)
@if (! in_array($egg->id, $mount->eggs->pluck('id')->toArray()))
<option value="{{ $egg->id }}">{{ $egg->name }}</option>
@endif
@endforeach
</optgroup>
@endforeach
</select>
</div>
</div>
</div>
<div class="modal-footer">
{!! csrf_field() !!}
<button type="button" class="btn btn-default btn-sm pull-left" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm">Add</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="addNodesModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="{{ route('admin.mounts.nodes', $mount->id) }}" method="POST">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true" style="color: #FFFFFF">&times;</span>
</button>
<h4 class="modal-title">Add Nodes</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="form-group col-md-12">
<label for="pNodes">Nodes</label>
<select id="pNodes" name="nodes[]" class="form-control" multiple>
@foreach ($locations as $location)
<optgroup label="{{ $location->long }} ({{ $location->short }})">
@foreach ($location->nodes as $node)
@if (! in_array($node->id, $mount->nodes->pluck('id')->toArray()))
<option value="{{ $node->id }}">{{ $node->name }}</option>
@endif
@endforeach
</optgroup>
@endforeach
</select>
</div>
</div>
</div>
<div class="modal-footer">
{!! csrf_field() !!}
<button type="button" class="btn btn-default btn-sm pull-left" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm">Add</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
$(document).ready(function() {
$('#pEggs').select2({
placeholder: 'Select eggs..',
});
$('#pNodes').select2({
placeholder: 'Select nodes..',
});
$('button[data-action="detach-egg"]').click(function (event) {
event.preventDefault();
const element = $(this);
const eggId = $(this).data('id');
$.ajax({
method: 'DELETE',
url: '/admin/mounts/' + {{ $mount->id }} + '/eggs/' + eggId,
headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') },
}).done(function () {
element.parent().parent().addClass('warning').delay(100).fadeOut();
swal({ type: 'success', title: 'Egg detached.' });
}).fail(function (jqXHR) {
console.error(jqXHR);
swal({
title: 'Whoops!',
text: jqXHR.responseJSON.error,
type: 'error'
});
});
});
$('button[data-action="detach-node"]').click(function (event) {
event.preventDefault();
const element = $(this);
const nodeId = $(this).data('id');
$.ajax({
method: 'DELETE',
url: '/admin/mounts/' + {{ $mount->id }} + '/nodes/' + nodeId,
headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') },
}).done(function () {
element.parent().parent().addClass('warning').delay(100).fadeOut();
swal({ type: 'success', title: 'Node detached.' });
}).fail(function (jqXHR) {
console.error(jqXHR);
swal({
title: 'Whoops!',
text: jqXHR.responseJSON.error,
type: 'error'
});
});
});
});
</script>
@endsection

View file

@ -21,6 +21,9 @@
<li class="{{ $router->currentRouteNamed('admin.servers.view.database') ? 'active' : '' }}">
<a href="{{ route('admin.servers.view.database', $server->id) }}">Database</a>
</li>
<li class="{{ $router->currentRouteNamed('admin.servers.view.mounts') ? 'active' : '' }}">
<a href="{{ route('admin.servers.view.mounts', $server->id) }}">Mounts</a>
</li>
@endif
<li class="{{ $router->currentRouteNamed('admin.servers.view.manage') ? 'active' : '' }}">
<a href="{{ route('admin.servers.view.manage', $server->id) }}">Manage</a>

View file

@ -37,7 +37,7 @@
<th>Username</th>
<th>Connections From</th>
<th>Host</th>
<th>Max Conenctions</th>
<th>Max Connections</th>
<th></th>
</tr>
@foreach($server->databases as $database)

View file

@ -0,0 +1,78 @@
@extends('layouts.admin')
@section('title')
Server {{ $server->name }}: Mounts
@endsection
@section('content-header')
<h1>{{ $server->name }}<small>Manage server mounts.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.servers') }}">Servers</a></li>
<li><a href="{{ route('admin.servers.view', $server->id) }}">{{ $server->name }}</a></li>
<li class="active">Mounts</li>
</ol>
@endsection
@section('content')
@include('admin.servers.partials.navigation')
<div class="row">
<div class="col-sm-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Available Mounts</h3>
</div>
<div class="box-body table-responsible no-padding">
<table class="table table-hover">
<tr>
<th>ID</th>
<th>Name</th>
<th>Source</th>
<th>Target</th>
<th>Status</th>
<th></th>
</tr>
@foreach ($mounts as $mount)
<tr>
<td class="col-sm-1 middle"><code>{{ $mount->id }}</code></td>
<td class="middle"><a href="{{ route('admin.mounts.view', $mount->id) }}">{{ $mount->name }}</a></td>
<td class="middle"><code>{{ $mount->source }}</code></td>
<td class="col-sm-2 middle"><code>{{ $mount->target }}</code></td>
@if (! in_array($mount->id, $server->mounts->pluck('id')->toArray()))
<td class="col-sm-2 middle">
<span class="label label-primary">Unmounted</span>
</td>
<td class="col-sm-1 middle">
<form action="{{ route('admin.servers.view.mounts.toggle', [ 'server' => $server->id, 'mount' => $mount->id ]) }}" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-xs btn-success"><i class="fa fa-plus"></i></button>
</form>
</td>
@else
<td class="col-sm-2 middle">
<span class="label label-success">Mounted</span>
</td>
<td class="col-sm-1 middle">
<form action="{{ route('admin.servers.view.mounts.toggle', [ 'server' => $server->id, 'mount' => $mount->id ]) }}" method="POST">
@method('DELETE')
{!! csrf_field() !!}
<button type="submit" class="btn btn-xs btn-danger"><i class="fa fa-times"></i></button>
</form>
</td>
@endif
</tr>
@endforeach
</table>
</div>
</div>
</div>
</div>
@endsection

View file

@ -117,6 +117,11 @@
</a>
</li>
<li class="header">SERVICE MANAGEMENT</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.mounts') ?: 'active' }}">
<a href="{{ route('admin.mounts') }}">
<i class="fa fa-magic"></i> <span>Mounts</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.nests') ?: 'active' }}">
<a href="{{ route('admin.nests') }}">
<i class="fa fa-th-large"></i> <span>Nests</span>