Fix up most of the file manager
This commit is contained in:
parent
042c28ca43
commit
83c776fc82
17 changed files with 1261 additions and 6 deletions
|
@ -142,6 +142,7 @@ class ServerController extends Controller
|
|||
{
|
||||
$server = Models\Server::getByUUID($uuid);
|
||||
$this->authorize('edit-files', $server);
|
||||
$node = Models\Node::find($server->node);
|
||||
|
||||
$fileInfo = (object) pathinfo($file);
|
||||
$controller = new FileRepository($uuid);
|
||||
|
@ -159,9 +160,15 @@ class ServerController extends Controller
|
|||
return redirect()->route('server.files.index', $uuid);
|
||||
}
|
||||
|
||||
Javascript::put([
|
||||
'server' => collect($server->makeVisible('daemonSecret'))->only(['uuid', 'uuidShort', 'daemonSecret', 'username']),
|
||||
'node' => collect($node)->only('fqdn', 'scheme', 'daemonListen'),
|
||||
'stat' => $fileContent['stat'],
|
||||
]);
|
||||
|
||||
return view('server.files.edit', [
|
||||
'server' => $server,
|
||||
'node' => Models\Node::find($server->node),
|
||||
'node' => $node,
|
||||
'file' => $file,
|
||||
'stat' => $fileContent['stat'],
|
||||
'contents' => $fileContent['file']->content,
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
"babel-preset-es2015": "6.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./node_modules/babel-cli/bin/babel.js public/js/files --source-maps --out-file public/js/filemanager.min.js"
|
||||
"build": "./node_modules/babel-cli/bin/babel.js public/themes/pterodactyl/js/frontend/files/src --source-maps --out-file public/themes/pterodactyl/js/frontend/files/filemanager.min.js"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,3 +61,45 @@ code {
|
|||
.middle {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
#fileOptionMenu.dropdown-menu > li > a {
|
||||
padding:3px 6px;
|
||||
}
|
||||
|
||||
.hasFileHover {
|
||||
border: 2px dashed #0087F7;
|
||||
border-top: 0 !important;
|
||||
border-radius: 5px;
|
||||
margin: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.hasFileHover * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
td.has-progress {
|
||||
padding: 0px !important;
|
||||
border-top: 0px !important;
|
||||
}
|
||||
|
||||
.progress.progress-table-bottom {
|
||||
margin: 0 !important;
|
||||
height:5px !important;
|
||||
padding:0;
|
||||
border:0;
|
||||
}
|
||||
|
||||
.muted {
|
||||
filter: alpha(opacity=20);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.muted-hover:hover {
|
||||
filter: alpha(opacity=100);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.use-pointer {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
|
73
public/themes/pterodactyl/js/frontend/files/editor.js
Normal file
73
public/themes/pterodactyl/js/frontend/files/editor.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
$(document).ready(function () {
|
||||
$('.server-files').addClass('active');
|
||||
const Editor = ace.edit('editor');
|
||||
const Modelist = ace.require('ace/ext/modelist')
|
||||
|
||||
Editor.setTheme('ace/theme/chrome');
|
||||
Editor.getSession().setMode(Modelist.getModeForPath(Pterodactyl.stat.name).mode);
|
||||
Editor.getSession().setUseWrapMode(true);
|
||||
Editor.setShowPrintMargin(false);
|
||||
|
||||
Editor.commands.addCommand({
|
||||
name: 'save',
|
||||
bindKey: {win: 'Ctrl-S', mac: 'Command-S'},
|
||||
exec: function(editor) {
|
||||
save();
|
||||
},
|
||||
readOnly: false
|
||||
});
|
||||
|
||||
$('#save_file').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
save();
|
||||
});
|
||||
|
||||
function save() {
|
||||
var fileName = $('input[name="file"]').val();
|
||||
$('#save_file').html('<i class="fa fw-fw fa-spinner fa-spin"></i> Saving File').addClass('disabled');
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: Router.route('server.files.save', { server: Pterodactyl.server.uuidShort }),
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'),
|
||||
},
|
||||
data: {
|
||||
file: fileName,
|
||||
contents: Editor.getValue()
|
||||
}
|
||||
}).done(function (data) {
|
||||
$.notify({
|
||||
message: 'File was successfully saved.'
|
||||
}, {
|
||||
type: 'success'
|
||||
});
|
||||
}).fail(function (jqXHR) {
|
||||
$.notify({
|
||||
message: jqXHR.responseText
|
||||
}, {
|
||||
type: 'danger'
|
||||
});
|
||||
}).always(function () {
|
||||
$('#save_file').html('<i class="fa fa-fw fa-save"></i> Save File').removeClass('disabled');
|
||||
});
|
||||
}
|
||||
});
|
2
public/themes/pterodactyl/js/frontend/files/filemanager.min.js
vendored
Normal file
2
public/themes/pterodactyl/js/frontend/files/filemanager.min.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
//# sourceMappingURL=filemanager.min.js.map
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"sources":[],"names":[],"mappings":"","file":"filemanager.min.js"}
|
394
public/themes/pterodactyl/js/frontend/files/src/actions.js
Normal file
394
public/themes/pterodactyl/js/frontend/files/src/actions.js
Normal file
|
@ -0,0 +1,394 @@
|
|||
"use strict";
|
||||
|
||||
// Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
class ActionsClass {
|
||||
constructor(element, menu) {
|
||||
this.element = element;
|
||||
this.menu = menu;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.element = undefined;
|
||||
}
|
||||
|
||||
folder() {
|
||||
const nameBlock = $(this.element).find('td[data-identifier="name"]');
|
||||
const currentName = decodeURIComponent(nameBlock.attr('data-name'));
|
||||
const currentPath = decodeURIComponent(nameBlock.data('path'));
|
||||
|
||||
let inputValue = `${currentPath}${currentName}/`;
|
||||
if ($(this.element).data('type') === 'file') {
|
||||
inputValue = currentPath;
|
||||
}
|
||||
swal({
|
||||
type: 'input',
|
||||
title: 'Create Folder',
|
||||
text: 'Please enter the path and folder name below.',
|
||||
showCancelButton: true,
|
||||
showConfirmButton: true,
|
||||
closeOnConfirm: false,
|
||||
showLoaderOnConfirm: true,
|
||||
inputValue: inputValue
|
||||
}, (val) => {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-Access-Token': Pterodactyl.server.daemonSecret,
|
||||
'X-Access-Server': Pterodactyl.server.uuid,
|
||||
},
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/folder`,
|
||||
timeout: 10000,
|
||||
data: JSON.stringify({
|
||||
path: val,
|
||||
}),
|
||||
}).done(data => {
|
||||
swal.close();
|
||||
Files.list();
|
||||
}).fail(jqXHR => {
|
||||
console.error(jqXHR);
|
||||
var error = 'An error occured while trying to process this request.';
|
||||
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
|
||||
error = jqXHR.responseJSON.error;
|
||||
}
|
||||
swal({
|
||||
type: 'error',
|
||||
title: '',
|
||||
text: error,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
move() {
|
||||
const nameBlock = $(this.element).find('td[data-identifier="name"]');
|
||||
const currentName = decodeURIComponent(nameBlock.attr('data-name'));
|
||||
const currentPath = decodeURIComponent(nameBlock.data('path'));
|
||||
|
||||
swal({
|
||||
type: 'input',
|
||||
title: 'Move File',
|
||||
text: 'Please enter the new path for the file below.',
|
||||
showCancelButton: true,
|
||||
showConfirmButton: true,
|
||||
closeOnConfirm: false,
|
||||
showLoaderOnConfirm: true,
|
||||
inputValue: `${currentPath}${currentName}`,
|
||||
}, (val) => {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-Access-Token': Pterodactyl.server.daemonSecret,
|
||||
'X-Access-Server': Pterodactyl.server.uuid,
|
||||
},
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/move`,
|
||||
timeout: 10000,
|
||||
data: JSON.stringify({
|
||||
from: `${currentPath}${currentName}`,
|
||||
to: `${val}`,
|
||||
}),
|
||||
}).done(data => {
|
||||
nameBlock.parent().addClass('warning').delay(200).fadeOut();
|
||||
swal.close();
|
||||
}).fail(jqXHR => {
|
||||
console.error(jqXHR);
|
||||
var error = 'An error occured while trying to process this request.';
|
||||
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
|
||||
error = jqXHR.responseJSON.error;
|
||||
}
|
||||
swal({
|
||||
type: 'error',
|
||||
title: '',
|
||||
text: error,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
rename() {
|
||||
const nameBlock = $(this.element).find('td[data-identifier="name"]');
|
||||
const currentLink = nameBlock.find('a');
|
||||
const currentName = decodeURIComponent(nameBlock.attr('data-name'));
|
||||
const attachEditor = `
|
||||
<input class="form-control input-sm" type="text" value="${currentName}" />
|
||||
<span class="input-loader"><i class="fa fa-refresh fa-spin fa-fw"></i></span>
|
||||
`;
|
||||
|
||||
nameBlock.html(attachEditor);
|
||||
const inputField = nameBlock.find('input');
|
||||
const inputLoader = nameBlock.find('.input-loader');
|
||||
|
||||
inputField.focus();
|
||||
inputField.on('blur keydown', e => {
|
||||
// Save Field
|
||||
if (
|
||||
(e.type === 'keydown' && e.which === 27)
|
||||
|| e.type === 'blur'
|
||||
|| (e.type === 'keydown' && e.which === 13 && currentName === inputField.val())
|
||||
) {
|
||||
if (!_.isEmpty(currentLink)) {
|
||||
nameBlock.html(currentLink);
|
||||
} else {
|
||||
nameBlock.html(currentName);
|
||||
}
|
||||
inputField.remove();
|
||||
ContextMenu.unbind().run();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.type === 'keydown' && e.which !== 13) return;
|
||||
|
||||
inputLoader.show();
|
||||
const currentPath = decodeURIComponent(nameBlock.data('path'));
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-Access-Token': Pterodactyl.server.daemonSecret,
|
||||
'X-Access-Server': Pterodactyl.server.uuid,
|
||||
},
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/rename`,
|
||||
timeout: 10000,
|
||||
data: JSON.stringify({
|
||||
from: `${currentPath}${currentName}`,
|
||||
to: `${currentPath}${inputField.val()}`,
|
||||
}),
|
||||
}).done(data => {
|
||||
nameBlock.attr('data-name', inputField.val());
|
||||
if (!_.isEmpty(currentLink)) {
|
||||
let newLink = currentLink.attr('href');
|
||||
if (nameBlock.parent().data('type') !== 'folder') {
|
||||
newLink = newLink.substr(0, newLink.lastIndexOf('/')) + '/' + inputField.val();
|
||||
}
|
||||
currentLink.attr('href', newLink);
|
||||
nameBlock.html(
|
||||
currentLink.html(inputField.val())
|
||||
);
|
||||
} else {
|
||||
nameBlock.html(inputField.val());
|
||||
}
|
||||
inputField.remove();
|
||||
}).fail(jqXHR => {
|
||||
console.error(jqXHR);
|
||||
var error = 'An error occured while trying to process this request.';
|
||||
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
|
||||
error = jqXHR.responseJSON.error;
|
||||
}
|
||||
nameBlock.addClass('has-error').delay(2000).queue(() => {
|
||||
nameBlock.removeClass('has-error').dequeue();
|
||||
});
|
||||
inputField.popover({
|
||||
animation: true,
|
||||
placement: 'top',
|
||||
content: error,
|
||||
title: 'Save Error'
|
||||
}).popover('show');
|
||||
}).always(() => {
|
||||
inputLoader.remove();
|
||||
ContextMenu.unbind().run();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
copy() {
|
||||
const nameBlock = $(this.element).find('td[data-identifier="name"]');
|
||||
const currentName = decodeURIComponent(nameBlock.attr('data-name'));
|
||||
const currentPath = decodeURIComponent(nameBlock.data('path'));
|
||||
|
||||
swal({
|
||||
type: 'input',
|
||||
title: 'Copy File',
|
||||
text: 'Please enter the new path for the copied file below.',
|
||||
showCancelButton: true,
|
||||
showConfirmButton: true,
|
||||
closeOnConfirm: false,
|
||||
showLoaderOnConfirm: true,
|
||||
inputValue: `${currentPath}${currentName}`,
|
||||
}, (val) => {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-Access-Token': Pterodactyl.server.daemonSecret,
|
||||
'X-Access-Server': Pterodactyl.server.uuid,
|
||||
},
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/copy`,
|
||||
timeout: 10000,
|
||||
data: JSON.stringify({
|
||||
from: `${currentPath}${currentName}`,
|
||||
to: `${val}`,
|
||||
}),
|
||||
}).done(data => {
|
||||
swal({
|
||||
type: 'success',
|
||||
title: '',
|
||||
text: 'File successfully copied.'
|
||||
});
|
||||
Files.list();
|
||||
}).fail(jqXHR => {
|
||||
console.error(jqXHR);
|
||||
var error = 'An error occured while trying to process this request.';
|
||||
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
|
||||
error = jqXHR.responseJSON.error;
|
||||
}
|
||||
swal({
|
||||
type: 'error',
|
||||
title: '',
|
||||
text: error,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
download() {
|
||||
const nameBlock = $(this.element).find('td[data-identifier="name"]');
|
||||
const fileName = decodeURIComponent(nameBlock.attr('data-name'));
|
||||
const filePath = decodeURIComponent(nameBlock.data('path'));
|
||||
|
||||
window.location = `/server/${Pterodactyl.server.uuidShort}/files/download/${filePath}${fileName}`;
|
||||
}
|
||||
|
||||
delete() {
|
||||
const nameBlock = $(this.element).find('td[data-identifier="name"]');
|
||||
const delPath = decodeURIComponent(nameBlock.data('path'));
|
||||
const delName = decodeURIComponent(nameBlock.data('name'));
|
||||
|
||||
swal({
|
||||
type: 'warning',
|
||||
title: '',
|
||||
text: 'Are you sure you want to delete <code>' + delName + '</code>? There is <strong>no</strong> reversing this action.',
|
||||
html: true,
|
||||
showCancelButton: true,
|
||||
showConfirmButton: true,
|
||||
closeOnConfirm: false,
|
||||
showLoaderOnConfirm: true
|
||||
}, () => {
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/f/${delPath}${delName}`,
|
||||
headers: {
|
||||
'X-Access-Token': Pterodactyl.server.daemonSecret,
|
||||
'X-Access-Server': Pterodactyl.server.uuid,
|
||||
}
|
||||
}).done(data => {
|
||||
nameBlock.parent().addClass('warning').delay(200).fadeOut();
|
||||
swal({
|
||||
type: 'success',
|
||||
title: 'File Deleted'
|
||||
});
|
||||
}).fail(jqXHR => {
|
||||
console.error(jqXHR);
|
||||
swal({
|
||||
type: 'error',
|
||||
title: 'Whoops!',
|
||||
html: true,
|
||||
text: 'An error occured while attempting to delete this file. Please try again.',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
decompress() {
|
||||
const nameBlock = $(this.element).find('td[data-identifier="name"]');
|
||||
const compPath = decodeURIComponent(nameBlock.data('path'));
|
||||
const compName = decodeURIComponent(nameBlock.data('name'));
|
||||
|
||||
swal({
|
||||
title: '<i class="fa fa-refresh fa-spin"></i> Decompressing...',
|
||||
text: 'This might take a few seconds to complete.',
|
||||
html: true,
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/decompress`,
|
||||
headers: {
|
||||
'X-Access-Token': Pterodactyl.server.daemonSecret,
|
||||
'X-Access-Server': Pterodactyl.server.uuid,
|
||||
},
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
data: JSON.stringify({
|
||||
files: `${compPath}${compName}`
|
||||
})
|
||||
}).done(data => {
|
||||
swal.close();
|
||||
Files.list(compPath);
|
||||
}).fail(jqXHR => {
|
||||
console.error(jqXHR);
|
||||
var error = 'An error occured while trying to process this request.';
|
||||
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
|
||||
error = jqXHR.responseJSON.error;
|
||||
}
|
||||
swal({
|
||||
type: 'error',
|
||||
title: 'Whoops!',
|
||||
html: true,
|
||||
text: error
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
compress() {
|
||||
const nameBlock = $(this.element).find('td[data-identifier="name"]');
|
||||
const compPath = decodeURIComponent(nameBlock.data('path'));
|
||||
const compName = decodeURIComponent(nameBlock.data('name'));
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/server/file/compress`,
|
||||
headers: {
|
||||
'X-Access-Token': Pterodactyl.server.daemonSecret,
|
||||
'X-Access-Server': Pterodactyl.server.uuid,
|
||||
},
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
data: JSON.stringify({
|
||||
files: `${compPath}${compName}`,
|
||||
to: compPath.toString()
|
||||
})
|
||||
}).done(data => {
|
||||
Files.list(compPath, err => {
|
||||
if (err) return;
|
||||
const fileListing = $('#file_listing').find(`[data-name="${data.saved_as}"]`).parent();
|
||||
fileListing.addClass('success pulsate').delay(3000).queue(() => {
|
||||
fileListing.removeClass('success pulsate').dequeue();
|
||||
});
|
||||
});
|
||||
}).fail(jqXHR => {
|
||||
console.error(jqXHR);
|
||||
var error = 'An error occured while trying to process this request.';
|
||||
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
|
||||
error = jqXHR.responseJSON.error;
|
||||
}
|
||||
swal({
|
||||
type: 'error',
|
||||
title: 'Whoops!',
|
||||
html: true,
|
||||
text: error
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
195
public/themes/pterodactyl/js/frontend/files/src/contextmenu.js
Normal file
195
public/themes/pterodactyl/js/frontend/files/src/contextmenu.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
"use strict";
|
||||
|
||||
// Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
class ContextMenuClass {
|
||||
constructor() {
|
||||
this.activeLine = null;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.directoryClick();
|
||||
this.rightClick();
|
||||
}
|
||||
|
||||
makeMenu(parent) {
|
||||
$(document).find('#fileOptionMenu').remove();
|
||||
if (!_.isNull(this.activeLine)) this.activeLine.removeClass('active');
|
||||
|
||||
let newFilePath = $('#headerTableRow').attr('data-currentDir');
|
||||
if (parent.data('type') === 'folder') {
|
||||
const nameBlock = parent.find('td[data-identifier="name"]');
|
||||
const currentName = decodeURIComponent(nameBlock.attr('data-name'));
|
||||
const currentPath = decodeURIComponent(nameBlock.data('path'));
|
||||
newFilePath = `${currentPath}${currentName}`;
|
||||
}
|
||||
|
||||
let buildMenu = '<ul id="fileOptionMenu" class="dropdown-menu" role="menu" style="display:none" >';
|
||||
|
||||
if (Pterodactyl.permissions.moveFiles) {
|
||||
buildMenu += '<li data-action="rename"><a tabindex="-1" href="#"><i class="fa fa-fw fa-pencil-square-o"></i> Rename</a></li> \
|
||||
<li data-action="move"><a tabindex="-1" href="#"><i class="fa fa-fw fa-arrow-right"></i> Move</a></li>';
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.copyFiles) {
|
||||
buildMenu += '<li data-action="copy"><a tabindex="-1" href="#"><i class="fa fa-fw fa-clone"></i> Copy</a></li>';
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.compressFiles) {
|
||||
buildMenu += '<li data-action="compress" class="hidden"><a tabindex="-1" href="#"><i class="fa fa-fw fa-file-archive-o"></i> Compress</a></li>';
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.decompressFiles) {
|
||||
buildMenu += '<li data-action="decompress" class="hidden"><a tabindex="-1" href="#"><i class="fa fa-fw fa-expand"></i> Decompress</a></li>';
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.createFiles) {
|
||||
buildMenu += '<li class="divider"></li> \
|
||||
<li data-action="file"><a href="/server/'+ Pterodactyl.server.uuidShort +'/files/add/?dir=' + newFilePath + '" class="text-muted"><i class="fa fa-fw fa-plus"></i> New File</a></li> \
|
||||
<li data-action="folder"><a tabindex="-1" href="#"><i class="fa fa-fw fa-folder"></i> New Folder</a></li>';
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.downloadFiles || Pterodactyl.permissions.deleteFiles) {
|
||||
buildMenu += '<li class="divider"></li>';
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.downloadFiles) {
|
||||
buildMenu += '<li data-action="download" class="hidden"><a tabindex="-1" href="#"><i class="fa fa-fw fa-download"></i> Download</a></li>';
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.deleteFiles) {
|
||||
buildMenu += '<li data-action="delete" class="bg-danger"><a tabindex="-1" href="#"><i class="fa fa-fw fa-trash-o"></i> Delete</a></li>';
|
||||
}
|
||||
|
||||
buildMenu += '</ul>';
|
||||
return buildMenu;
|
||||
}
|
||||
|
||||
rightClick() {
|
||||
$('[data-action="toggleMenu"]').on('mousedown', () => {
|
||||
event.preventDefault();
|
||||
this.showMenu(event);
|
||||
});
|
||||
$('#file_listing > tbody td').on('contextmenu', event => {
|
||||
this.showMenu(event);
|
||||
});
|
||||
}
|
||||
|
||||
showMenu(event) {
|
||||
const parent = $(event.target).closest('tr');
|
||||
const menu = $(this.makeMenu(parent));
|
||||
|
||||
if (parent.data('type') === 'disabled') return;
|
||||
event.preventDefault();
|
||||
|
||||
$(menu).appendTo('body');
|
||||
$(menu).data('invokedOn', $(event.target)).show().css({
|
||||
position: 'absolute',
|
||||
left: event.pageX - 150,
|
||||
top: event.pageY,
|
||||
});
|
||||
|
||||
this.activeLine = parent;
|
||||
this.activeLine.addClass('active');
|
||||
|
||||
// Handle Events
|
||||
const Actions = new ActionsClass(parent, menu);
|
||||
if (Pterodactyl.permissions.moveFiles) {
|
||||
$(menu).find('li[data-action="move"]').unbind().on('click', e => {
|
||||
e.preventDefault();
|
||||
Actions.move();
|
||||
});
|
||||
$(menu).find('li[data-action="rename"]').unbind().on('click', e => {
|
||||
e.preventDefault();
|
||||
Actions.rename();
|
||||
});
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.copyFiles) {
|
||||
$(menu).find('li[data-action="copy"]').unbind().on('click', e => {
|
||||
e.preventDefault();
|
||||
Actions.copy();
|
||||
});
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.compressFiles) {
|
||||
if (parent.data('type') === 'folder') {
|
||||
$(menu).find('li[data-action="compress"]').removeClass('hidden');
|
||||
}
|
||||
$(menu).find('li[data-action="compress"]').unbind().on('click', e => {
|
||||
e.preventDefault();
|
||||
Actions.compress();
|
||||
});
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.decompressFiles) {
|
||||
if (_.without(['application/zip', 'application/gzip', 'application/x-gzip'], parent.data('mime')).length < 3) {
|
||||
$(menu).find('li[data-action="decompress"]').removeClass('hidden');
|
||||
}
|
||||
$(menu).find('li[data-action="decompress"]').unbind().on('click', e => {
|
||||
e.preventDefault();
|
||||
Actions.decompress();
|
||||
});
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.createFiles) {
|
||||
$(menu).find('li[data-action="folder"]').unbind().on('click', e => {
|
||||
e.preventDefault();
|
||||
Actions.folder();
|
||||
});
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.downloadFiles) {
|
||||
if (parent.data('type') === 'file') {
|
||||
$(menu).find('li[data-action="download"]').removeClass('hidden');
|
||||
}
|
||||
$(menu).find('li[data-action="download"]').unbind().on('click', e => {
|
||||
e.preventDefault();
|
||||
Actions.download();
|
||||
});
|
||||
}
|
||||
|
||||
if (Pterodactyl.permissions.deleteFiles) {
|
||||
$(menu).find('li[data-action="delete"]').unbind().on('click', e => {
|
||||
e.preventDefault();
|
||||
Actions.delete();
|
||||
});
|
||||
}
|
||||
|
||||
$(window).on('click', () => {
|
||||
$(menu).remove();
|
||||
if(!_.isNull(this.activeLine)) this.activeLine.removeClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
directoryClick() {
|
||||
$('a[data-action="directory-view"]').on('click', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
const path = $(this).parent().data('path') || '';
|
||||
const name = $(this).parent().data('name') || '';
|
||||
|
||||
window.location.hash = encodeURIComponent(path + name);
|
||||
Files.list();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.ContextMenu = new ContextMenuClass;
|
87
public/themes/pterodactyl/js/frontend/files/src/index.js
Normal file
87
public/themes/pterodactyl/js/frontend/files/src/index.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
"use strict";
|
||||
|
||||
// Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
class FileManager {
|
||||
constructor() {
|
||||
this.list(this.decodeHash());
|
||||
}
|
||||
|
||||
list(path, next) {
|
||||
if (_.isUndefined(path)) {
|
||||
path = this.decodeHash();
|
||||
}
|
||||
|
||||
this.loader(true);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: Pterodactyl.meta.directoryList,
|
||||
headers: {
|
||||
'X-CSRF-Token': Pterodactyl.meta.csrftoken,
|
||||
},
|
||||
data: {
|
||||
directory: path,
|
||||
},
|
||||
}).done(data => {
|
||||
this.loader(false);
|
||||
$('#load_files').slideUp(10).html(data).slideDown(10, () => {
|
||||
ContextMenu.run();
|
||||
this.reloadFilesButton();
|
||||
if (_.isFunction(next)) {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
$('#internal_alert').slideUp();
|
||||
}).fail(jqXHR => {
|
||||
this.loader(false);
|
||||
if (_.isFunction(next)) {
|
||||
return next(new Error('Failed to load file listing.'));
|
||||
}
|
||||
swal({
|
||||
type: 'error',
|
||||
title: 'File Error',
|
||||
text: 'An error occured while attempting to process this request. Please try again.',
|
||||
});
|
||||
console.error(jqXHR);
|
||||
});
|
||||
}
|
||||
|
||||
loader(show) {
|
||||
if (show){
|
||||
$('.file-overlay').fadeIn(100);
|
||||
} else {
|
||||
$('.file-overlay').fadeOut(100);
|
||||
}
|
||||
}
|
||||
|
||||
reloadFilesButton() {
|
||||
$('i[data-action="reload-files"]').unbind().on('click', () => {
|
||||
$('i[data-action="reload-files"]').addClass('fa-spin');
|
||||
this.list();
|
||||
});
|
||||
}
|
||||
|
||||
decodeHash() {
|
||||
return decodeURIComponent(window.location.hash.substring(1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.Files = new FileManager;
|
129
public/themes/pterodactyl/js/frontend/files/upload.js
Normal file
129
public/themes/pterodactyl/js/frontend/files/upload.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
// Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
(function initUploader() {
|
||||
var notifyUploadSocketError = false;
|
||||
uploadSocket = io(Pterodactyl.node.scheme + '://' + Pterodactyl.node.fqdn + ':' + Pterodactyl.node.daemonListen + '/upload/' + Pterodactyl.server.uuid, {
|
||||
'query': 'token=' + Pterodactyl.server.daemonSecret,
|
||||
});
|
||||
|
||||
uploadSocket.io.on('connect_error', function (err) {
|
||||
if(typeof notifyUploadSocketError !== 'object') {
|
||||
notifyUploadSocketError = $.notify({
|
||||
message: 'There was an error attempting to establish a connection to the uploader endpoint.<br /><br />' + err,
|
||||
}, {
|
||||
type: 'danger',
|
||||
delay: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
uploadSocket.on('error', err => {
|
||||
siofu.destroy();
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
uploadSocket.on('connect', function () {
|
||||
if (notifyUploadSocketError !== false) {
|
||||
notifyUploadSocketError.close();
|
||||
notifyUploadSocketError = false;
|
||||
}
|
||||
});
|
||||
|
||||
var siofu = new SocketIOFileUpload(uploadSocket);
|
||||
siofu.listenOnDrop(document.getElementById("load_files"));
|
||||
|
||||
window.addEventListener('dragover', function (event) {
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
|
||||
window.addEventListener('drop', function (event) {
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
|
||||
var dropCounter = 0;
|
||||
$('#load_files').bind({
|
||||
dragenter: function (event) {
|
||||
event.preventDefault();
|
||||
dropCounter++;
|
||||
$(this).addClass('hasFileHover');
|
||||
},
|
||||
dragleave: function (event) {
|
||||
dropCounter--;
|
||||
if (dropCounter === 0) {
|
||||
$(this).removeClass('hasFileHover');
|
||||
}
|
||||
},
|
||||
drop: function (event) {
|
||||
dropCounter = 0;
|
||||
$(this).removeClass('hasFileHover');
|
||||
}
|
||||
});
|
||||
|
||||
siofu.addEventListener('start', function (event) {
|
||||
event.file.meta.path = $('#headerTableRow').attr('data-currentdir');
|
||||
event.file.meta.identifier = Math.random().toString(36).slice(2);
|
||||
|
||||
$('#append_files_to').append('<tr id="file-upload-' + event.file.meta.identifier +'"> \
|
||||
<td><i class="fa fa-file-text-o" style="margin-left: 2px;"></i></td> \
|
||||
<td>' + event.file.name + '</td> \
|
||||
<td colspan=2"> </td> \
|
||||
</tr><tr> \
|
||||
<td colspan="4" class="has-progress"> \
|
||||
<div class="progress progress-table-bottom active"> \
|
||||
<div class="progress-bar progress-bar-info prog-bar-' + event.file.meta.identifier +'" style="width: 0%"></div> \
|
||||
</div> \
|
||||
</td> \
|
||||
</tr>\
|
||||
');
|
||||
});
|
||||
|
||||
siofu.addEventListener('progress', function(event) {
|
||||
var percent = event.bytesLoaded / event.file.size * 100;
|
||||
if (percent >= 100) {
|
||||
$('.prog-bar-' + event.file.meta.identifier).css('width', '100%').removeClass('progress-bar-info').addClass('progress-bar-success').parent().removeClass('active');
|
||||
} else {
|
||||
$('.prog-bar-' + event.file.meta.identifier).css('width', percent + '%');
|
||||
}
|
||||
});
|
||||
|
||||
// Do something when a file is uploaded:
|
||||
siofu.addEventListener('complete', function(event){
|
||||
if (!event.success) {
|
||||
$('.prog-bar-' + event.file.meta.identifier).css('width', '100%').removeClass('progress-bar-info').addClass('progress-bar-danger');
|
||||
$.notify({
|
||||
message: 'An error was encountered while attempting to upload this file.'
|
||||
}, {
|
||||
type: 'danger',
|
||||
delay: 5000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
siofu.addEventListener('error', function(event){
|
||||
console.error(event);
|
||||
$('.prog-bar-' + event.file.meta.identifier).css('width', '100%').removeClass('progress-bar-info').addClass('progress-bar-danger');
|
||||
$.notify({
|
||||
message: 'An error was encountered while attempting to upload this file: <strong>' + event.message + '.</strong>',
|
||||
}, {
|
||||
type: 'danger',
|
||||
delay: 8000
|
||||
});
|
||||
});
|
||||
})();
|
15
public/themes/pterodactyl/vendor/siofu/client.min.js
vendored
Executable file
15
public/themes/pterodactyl/vendor/siofu/client.min.js
vendored
Executable file
|
@ -0,0 +1,15 @@
|
|||
/* Socket IO File Upload Client-Side Library
|
||||
* Copyright (C) 2015 Shane Carr and others
|
||||
* Released under the X11 License
|
||||
* For more information, visit: https://github.com/vote539/socketio-file-upload
|
||||
*/
|
||||
(function(g,d,f){"function"===typeof define&&define.amd?define([],f):"object"===typeof module&&module.exports?module.exports=f():g[d]=f()})(this,"SocketIOFileUpload",function(){return function(g){var d=this;if(!window.File||!window.FileReader)throw Error("Socket.IO File Upload: Browser Not Supported");window.siofu_global||(window.siofu_global={instances:0,downloads:0});var f={},p={},y={},q={},h={};d.fileInputElementId="siofu_input_"+siofu_global.instances++;d.resetFileInputs=!0;d.useText=!1;d.serializedOctets=
|
||||
!1;d.useBuffer=!0;d.chunkSize=102400;var t=function(a,b){var c=document.createEvent("Event");c.initEvent(a,!1,!1);for(var v in b)b.hasOwnProperty(v)&&(c[v]=b[v]);return d.dispatchEvent(c)},r=[],e=function(a,b,c,d){a.addEventListener(b,c,d);r.push(arguments)},z=function(a,b,c,d){a.removeEventListener&&a.removeEventListener(b,c,d)},B=function(){for(var a=r.length-1;0<=a;a--)z.apply(this,r[a]);r=[]},C=function(a){if(null!==d.maxFileSize&&a.size>d.maxFileSize)t("error",{file:a,message:"Attempt by client to upload file exceeding the maximum file size",
|
||||
code:1});else if(t("start",{file:a})){var b=new FileReader,c=siofu_global.downloads++,f=!1,h=d.useText,w=0,r;b._realReader&&(b=b._realReader);p[c]=a;var u={id:c},x=d.chunkSize;if(x>=a.size||0>=x)x=a.size;var n=function(){if(!u.abort){var c=a.slice(w,Math.min(w+x,a.size));h?b.readAsText(c):b.readAsArrayBuffer(c)}},A=function(e){if(!u.abort){var v=Math.min(w+x,a.size);a:{var p=w;e=e.target.result;var n=!1;if(!h)try{var l=new Uint8Array(e);if(d.serializedOctets)e=l;else if(d.useBuffer)e=l.buffer;else{var n=
|
||||
!0,m,q=l.buffer.byteLength,k="";for(m=0;m<q;m+=3)k+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[l[m]>>2],k+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(l[m]&3)<<4|l[m+1]>>4],k+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(l[m+1]&15)<<2|l[m+2]>>6],k+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[l[m+2]&63];2===q%3?k=k.substring(0,k.length-1)+"=":1===q%3&&(k=k.substring(0,k.length-2)+"==");e=k}}catch(E){g.emit("siofu_done",
|
||||
{id:c,interrupt:!0});break a}g.emit("siofu_progress",{id:c,size:a.size,start:p,end:v,content:e,base64:n})}t("progress",{file:a,bytesLoaded:v,name:r});w+=x;w>=a.size&&(g.emit("siofu_done",{id:c}),t("load",{file:a,reader:b,name:r}),f=!0)}};e(b,"load",A);e(b,"error",function(){g.emit("siofu_done",{id:c,interrupt:!0});z(b,"load",A)});e(b,"abort",function(){g.emit("siofu_done",{id:c,interrupt:!0});z(b,"load",A)});g.emit("siofu_start",{name:a.name,mtime:a.lastModified,meta:a.meta,size:a.size,encoding:h?
|
||||
"text":"octet",id:c});q[c]=function(a){r=a;n()};y[c]=function(){f||n()};return u}},u=function(a){if(0!==a.length){for(var b=0;b<a.length;b++)a[b].meta||(a[b].meta={});if(t("choose",{files:a}))for(b=0;b<a.length;b++){var c=C(a[b]);h[c.id]=c}}},n=function(a){var b=a.target.files||a.dataTransfer.files;a.preventDefault();u(b);if(d.resetFileInputs){try{a.target.value=""}catch(D){}if(a.target.value){var b=document.createElement("form"),c=a.target.parentNode,e=a.target.nextSibling;b.appendChild(a.target);
|
||||
b.reset();c.insertBefore(a.target,e)}}};this.submitFiles=function(a){a&&u(a)};this.listenOnSubmit=function(a,b){b.files&&e(a,"click",function(){u(b.files)},!1)};this.listenOnArraySubmit=function(a,b){for(var c in b)this.listenOnSubmit(a,b[c])};this.listenOnInput=function(a){a.files&&e(a,"change",n,!1)};this.listenOnDrop=function(a){e(a,"dragover",function(a){a.preventDefault()},!1);e(a,"drop",n)};this.prompt=function(){var a;a=document.getElementById(d.fileInputElementId);a||(a=document.createElement("input"),
|
||||
a.setAttribute("type","file"),a.setAttribute("id",d.fileInputElementId),a.style.display="none",document.body.appendChild(a));e(a,"change",n,!1);var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,0,0,!1,!1,!1,!1,0,null);a.dispatchEvent(b)};this.destroy=function(){B();var a=document.getElementById(d.fileInputElementId);a&&a.parentNode.removeChild(a);for(var b in h)h.hasOwnProperty(b)&&(h[b].abort=!0);h=q=p=f=null};this.addEventListener=function(a,b){f[a]||(f[a]=[]);
|
||||
f[a].push(b)};this.removeEventListener=function(a,b){if(!f[a])return!1;for(var c=0;c<f[a].length;c++)if(f[a][c]===b)return f[a].splice(c,1),!0;return!1};this.dispatchEvent=function(a){var b=f[a.type];if(!b)return!0;for(var c=!0,d=0;d<b.length;d++)!1===b[d](a)&&(c=!1);return c};e(g,"siofu_chunk",function(a){if(y[a.id])y[a.id]()});e(g,"siofu_ready",function(a){if(q[a.id])q[a.id](a.name)});e(g,"siofu_complete",function(a){p[a.id]&&t("complete",{file:p[a.id],detail:a.detail,success:a.success})});e(g,
|
||||
"siofu_error",function(a){p[a.id]&&(t("error",{file:p[a.id],message:a.message,code:0}),h&&(h[a.id].abort=!0))})}});
|
|
@ -23,5 +23,6 @@ return [
|
|||
'sftp_settings' => 'SFTP Settings',
|
||||
'startup_parameters' => 'Startup Parameters',
|
||||
'databases' => 'Databases',
|
||||
'edit_file' => 'Edit File',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -6,6 +6,23 @@ return [
|
|||
'header' => 'Server Console',
|
||||
'header_sub' => 'Control your server in real time.',
|
||||
],
|
||||
'files' => [
|
||||
'header' => 'File Manager',
|
||||
'header_sub' => 'Manage all of your files directly from the web.',
|
||||
'loading' => 'Loading initial file structure, this could take a few seconds.',
|
||||
'path' => 'When configuring any file paths in your server plugins or settings you should use :path as your base path. The maximum size for web-based file uploads to this node is :size.',
|
||||
'seconds_ago' => 'seconds ago',
|
||||
'file_name' => 'File Name',
|
||||
'size' => 'Size',
|
||||
'last_modified' => 'Last Modified',
|
||||
'add_new' => 'Add New File',
|
||||
'edit' => [
|
||||
'header' => 'Edit File',
|
||||
'header_sub' => 'Make modifications to a file from the web.',
|
||||
'save' => 'Save File',
|
||||
'return' => 'Return to File Manager',
|
||||
],
|
||||
],
|
||||
'config' => [
|
||||
'startup' => [
|
||||
'header' => 'Start Configuration',
|
||||
|
|
|
@ -140,7 +140,11 @@
|
|||
<i class="fa fa-terminal"></i> <span>@lang('navigation.server.console')</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="treeview {{ Route::currentRouteName() !== 'server.files.index' ?: 'active' }}">
|
||||
<li class="treeview
|
||||
@if(in_array(Route::currentRouteName(), ['server.files.index', 'server.files.edit', 'server.files.add']))
|
||||
active
|
||||
@endif
|
||||
">
|
||||
<a href="#">
|
||||
<i class="fa fa-files-o"></i>
|
||||
<span>@lang('navigation.server.file_management')</span>
|
||||
|
@ -149,9 +153,8 @@
|
|||
</span>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
<li><a href="{{ route('server.files.index', $server->uuidShort) }}"><i class="fa fa-angle-right"></i> @lang('navigation.server.file_browser')</a></li>
|
||||
<li><a href="{{ route('server.files.add', $server->uuidShort) }}"><i class="fa fa-angle-right"></i> @lang('navigation.server.create_file')</a></li>
|
||||
<li><a href=""><i class="fa fa-angle-right"></i> @lang('navigation.server.upload_files')</a></li>
|
||||
<li class="{{ (Route::currentRouteName() !== 'server.files.index' && Route::currentRouteName() !== 'server.files.edit') ?: 'active' }}"><a href="{{ route('server.files.index', $server->uuidShort) }}"><i class="fa fa-angle-right"></i> @lang('navigation.server.file_browser')</a></li>
|
||||
<li class="{{ Route::currentRouteName() !== 'server.files.add' ?: 'active' }}"><a href="{{ route('server.files.add', $server->uuidShort) }}"><i class="fa fa-angle-right"></i> @lang('navigation.server.create_file')</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
|
|
63
resources/themes/pterodactyl/server/files/edit.blade.php
Normal file
63
resources/themes/pterodactyl/server/files/edit.blade.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
{{-- Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com> --}}
|
||||
|
||||
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
|
||||
{{-- of this software and associated documentation files (the "Software"), to deal --}}
|
||||
{{-- in the Software without restriction, including without limitation the rights --}}
|
||||
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
|
||||
{{-- copies of the Software, and to permit persons to whom the Software is --}}
|
||||
{{-- furnished to do so, subject to the following conditions: --}}
|
||||
|
||||
{{-- The above copyright notice and this permission notice shall be included in all --}}
|
||||
{{-- copies or substantial portions of the Software. --}}
|
||||
|
||||
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
|
||||
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
|
||||
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
|
||||
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
|
||||
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
|
||||
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
|
||||
{{-- SOFTWARE. --}}
|
||||
@extends('layouts.master')
|
||||
|
||||
@section('title')
|
||||
@lang('server.files.edit.header')
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>@lang('server.files.edit.header')<small>@lang('server.files.edit.header_sub')</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('index') }}">@lang('strings.home')</a></li>
|
||||
<li><a href="{{ route('server.index', $server->uuidShort) }}">{{ $server->name }}</a></li>
|
||||
<li><a href="{{ route('server.files.index', $server->uuidShort) }}">@lang('navigation.server.file_browser')</a></li>
|
||||
<li class="active">@lang('navigation.server.edit_file')</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{{ $file }}</h3>
|
||||
<div class="pull-right box-tools">
|
||||
<a href="/server/{{ $server->uuidShort }}/files#{{ rawurlencode($directory) }}" class="pull-right"><button class="btn btn-default btn-sm">{{ trans('server.files.edit.return') }}</button></a>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="file" value="{{ $file }}" />
|
||||
<div class="box-body" style="height:500px;" id="editor">{{ $contents }}</div>
|
||||
<div class="box-footer with-border">
|
||||
<button class="btn btn-sm btn-primary" id="save_file"><i class="fa fa-fw fa-save"></i> @lang('server.files.edit.save')</button>
|
||||
<a href="/server/{{ $server->uuidShort }}/files#{{ rawurlencode($directory) }}" class="pull-right"><button class="btn btn-default btn-sm">{{ trans('server.files.edit.return') }}</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('footer-scripts')
|
||||
@parent
|
||||
{!! Theme::js('js/frontend/server.socket.js') !!}
|
||||
{!! Theme::js('js/vendor/ace/ace.js') !!}
|
||||
{!! Theme::js('js/vendor/ace/ext-modelist.js') !!}
|
||||
{!! Theme::js('js/frontend/files/editor.js') !!}
|
||||
@endsection
|
66
resources/themes/pterodactyl/server/files/index.blade.php
Normal file
66
resources/themes/pterodactyl/server/files/index.blade.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
{{-- Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com> --}}
|
||||
|
||||
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
|
||||
{{-- of this software and associated documentation files (the "Software"), to deal --}}
|
||||
{{-- in the Software without restriction, including without limitation the rights --}}
|
||||
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
|
||||
{{-- copies of the Software, and to permit persons to whom the Software is --}}
|
||||
{{-- furnished to do so, subject to the following conditions: --}}
|
||||
|
||||
{{-- The above copyright notice and this permission notice shall be included in all --}}
|
||||
{{-- copies or substantial portions of the Software. --}}
|
||||
|
||||
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
|
||||
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
|
||||
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
|
||||
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
|
||||
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
|
||||
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
|
||||
{{-- SOFTWARE. --}}
|
||||
@extends('layouts.master')
|
||||
|
||||
@section('title')
|
||||
@lang('server.files.header')
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>@lang('server.files.header')<small>@lang('server.files.header_sub')</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('index') }}">@lang('strings.home')</a></li>
|
||||
<li><a href="{{ route('server.index', $server->uuidShort) }}">{{ $server->name }}</a></li>
|
||||
<li>@lang('navigation.server.file_management')</li>
|
||||
<li class="active">@lang('navigation.server.file_browser')</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="overlay file-overlay"><i class="fa fa-refresh fa-spin"></i></div>
|
||||
<div class="box-body table-responsive no-padding" id="load_files">
|
||||
<div class="callout callout-info" style="margin:10px;">@lang('server.files.loading')</div>
|
||||
</div>
|
||||
<div class="box-footer with-border">
|
||||
<p class="text-muted small" style="margin: 0 0 2px;">@lang('server.files.path', ['path' => '<code>/home/container</code>', 'size' => '<code>' . $node->upload_size . ' MB</code>'])</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('footer-scripts')
|
||||
@parent
|
||||
{!! Theme::js('js/frontend/server.socket.js') !!}
|
||||
{!! Theme::js('js/vendor/async/async.min.js') !!}
|
||||
{!! Theme::js('js/vendor/lodash/lodash.js') !!}
|
||||
{!! Theme::js('vendor/siofu/client.min.js') !!}
|
||||
@if(App::environment('production'))
|
||||
{!! Theme::js('js/frontend/files/filemanager.min.js') !!}
|
||||
@else
|
||||
{!! Theme::js('js/frontend/files/src/index.js') !!}
|
||||
{!! Theme::js('js/frontend/files/src/contextmenu.js') !!}
|
||||
{!! Theme::js('js/frontend/files/src/actions.js') !!}
|
||||
@endif
|
||||
{!! Theme::js('js/frontend/files/upload.js') !!}
|
||||
@endsection
|
160
resources/themes/pterodactyl/server/files/list.blade.php
Normal file
160
resources/themes/pterodactyl/server/files/list.blade.php
Normal file
|
@ -0,0 +1,160 @@
|
|||
{{-- Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com> --}}
|
||||
|
||||
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
|
||||
{{-- of this software and associated documentation files (the "Software"), to deal --}}
|
||||
{{-- in the Software without restriction, including without limitation the rights --}}
|
||||
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
|
||||
{{-- copies of the Software, and to permit persons to whom the Software is --}}
|
||||
{{-- furnished to do so, subject to the following conditions: --}}
|
||||
|
||||
{{-- The above copyright notice and this permission notice shall be included in all --}}
|
||||
{{-- copies or substantial portions of the Software. --}}
|
||||
|
||||
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
|
||||
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
|
||||
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
|
||||
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
|
||||
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
|
||||
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
|
||||
{{-- SOFTWARE. --}}
|
||||
<table class="table table-hover" id="file_listing">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:2%;text-align:center;" class="middle"><i class="fa fa-refresh muted muted-hover use-pointer" data-action="reload-files"></i></th>
|
||||
<th style="width:55%">@lang('server.files.file_name')</th>
|
||||
<th style="width:15%">@lang('server.files.size')</th>
|
||||
<th style="width:20%">@lang('server.files.last_modified')</th>
|
||||
<th style="width:8%"></th>
|
||||
</tr>
|
||||
<tr id="headerTableRow" data-currentdir="{{ $directory['header'] }}">
|
||||
<th><i class="fa fa-folder-open"></i></th>
|
||||
<th colspan="4">
|
||||
<code>/home/container{{ $directory['header'] }}</code>
|
||||
<small>
|
||||
<a href="/server/{{ $server->uuidShort }}/files/add/@if($directory['header'] !== '')?dir={{ $directory['header'] }}@endif" class="text-muted">
|
||||
<i class="fa fa-plus" data-toggle="tooltip" data-placement="top" title="@lang('server.files.add_new')"></i>
|
||||
</a>
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="append_files_to">
|
||||
@if (isset($directory['first']) && $directory['first'] === true)
|
||||
<tr data-type="disabled">
|
||||
<td><i class="fa fa-folder" style="margin-left: 0.859px;"></i></td>
|
||||
<td><a href="/server/{{ $server->uuidShort }}/files" data-action="directory-view">←</a></a></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@endif
|
||||
@if (isset($directory['show']) && $directory['show'] === true)
|
||||
<tr data-type="disabled">
|
||||
<td><i class="fa fa-folder" style="margin-left: 0.859px;"></i></td>
|
||||
<td data-name="{{ rawurlencode($directory['link']) }}">
|
||||
<a href="/server/{{ $server->uuidShort }}/files" data-action="directory-view">← {{ $directory['link_show'] }}</a>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@endif
|
||||
@foreach ($folders as $folder)
|
||||
<tr data-type="folder">
|
||||
<td data-identifier="type" class="middle"><i class="fa fa-folder" style="margin-left: 0.859px;"></i></td>
|
||||
<td data-identifier="name" data-name="{{ rawurlencode($folder['entry']) }}" data-path="@if($folder['directory'] !== ''){{ rawurlencode($folder['directory']) }}@endif/">
|
||||
<a href="/server/{{ $server->uuidShort }}/files" data-action="directory-view">{{ $folder['entry'] }}</a>
|
||||
</td>
|
||||
<td data-identifier="size">{{ $folder['size'] }}</td>
|
||||
<td data-identifier="modified">
|
||||
<?php $carbon = Carbon::createFromTimestamp($folder['date'])->timezone(env('APP_TIMEZONE', 'America/New_York')); ?>
|
||||
@if($carbon->diffInMinutes(Carbon::now()) > 60)
|
||||
{{ $carbon->format('m/d/y H:i:s') }}
|
||||
@elseif($carbon->diffInSeconds(Carbon::now()) < 5 || $carbon->isFuture())
|
||||
<em>@lang('server.files.seconds_ago')</em>
|
||||
@else
|
||||
{{ $carbon->diffForHumans() }}
|
||||
@endif
|
||||
</td>
|
||||
<td><button class="btn btn-xxs btn-default" data-action="toggleMenu" style="padding:2px 6px 0px;"><i class="fa fa-ellipsis-h"></i></button></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@foreach ($files as $file)
|
||||
<tr data-type="file" data-mime="{{ $file['mime'] }}">
|
||||
<td data-identifier="type" class="middle">
|
||||
{{-- oh boy --}}
|
||||
@if(in_array($file['mime'], [
|
||||
'application/x-7z-compressed',
|
||||
'application/zip',
|
||||
'application/x-compressed-zip',
|
||||
'application/x-tar',
|
||||
'application/x-gzip',
|
||||
'application/x-bzip',
|
||||
'application/x-bzip2',
|
||||
'application/java-archive'
|
||||
]))
|
||||
<i class="fa fa-file-archive-o" style="margin-left: 2px;"></i>
|
||||
@elseif(in_array($file['mime'], [
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'application/xml',
|
||||
'application/xhtml+xml',
|
||||
'text/xml',
|
||||
'text/css',
|
||||
'text/html',
|
||||
'text/x-perl',
|
||||
'text/x-shellscript'
|
||||
]))
|
||||
<i class="fa fa-file-code-o" style="margin-left: 2px;"></i>
|
||||
@elseif(starts_with($file['mime'], 'image'))
|
||||
<i class="fa fa-file-image-o" style="margin-left: 2px;"></i>
|
||||
@elseif(starts_with($file['mime'], 'video'))
|
||||
<i class="fa fa-file-video-o" style="margin-left: 2px;"></i>
|
||||
@elseif(starts_with($file['mime'], 'video'))
|
||||
<i class="fa fa-file-audio-o" style="margin-left: 2px;"></i>
|
||||
@elseif(starts_with($file['mime'], 'application/vnd.ms-powerpoint'))
|
||||
<i class="fa fa-file-powerpoint-o" style="margin-left: 2px;"></i>
|
||||
@elseif(in_array($file['mime'], [
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||
'application/msword'
|
||||
]) || starts_with($file['mime'], 'application/vnd.ms-word'))
|
||||
<i class="fa fa-file-word-o" style="margin-left: 2px;"></i>
|
||||
@elseif(in_array($file['mime'], [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
]) || starts_with($file['mime'], 'application/vnd.ms-excel'))
|
||||
<i class="fa fa-file-excel-o" style="margin-left: 2px;"></i>
|
||||
@elseif($file['mime'] === 'application/pdf')
|
||||
<i class="fa fa-file-pdf-o" style="margin-left: 2px;"></i>
|
||||
@else
|
||||
<i class="fa fa-file-text-o" style="margin-left: 2px;"></i>
|
||||
@endif
|
||||
</td>
|
||||
<td data-identifier="name" data-name="{{ rawurlencode($file['entry']) }}" data-path="@if($file['directory'] !== ''){{ rawurlencode($file['directory']) }}@endif/">
|
||||
@if(in_array($file['mime'], $editableMime))
|
||||
@can('edit-files', $server)
|
||||
<a href="/server/{{ $server->uuidShort }}/files/edit/@if($file['directory'] !== ''){{ rawurlencode($file['directory']) }}/@endif{{ rawurlencode($file['entry']) }}" class="edit_file">{{ $file['entry'] }}</a>
|
||||
@else
|
||||
{{ $file['entry'] }}
|
||||
@endcan
|
||||
@else
|
||||
{{ $file['entry'] }}
|
||||
@endif
|
||||
</td>
|
||||
<td data-identifier="size">{{ $file['size'] }}</td>
|
||||
<td data-identifier="modified">
|
||||
<?php $carbon = Carbon::createFromTimestamp($file['date'])->timezone(env('APP_TIMEZONE', 'America/New_York')); ?>
|
||||
@if($carbon->diffInMinutes(Carbon::now()) > 60)
|
||||
{{ $carbon->format('m/d/y H:i:s') }}
|
||||
@elseif($carbon->diffInSeconds(Carbon::now()) < 5 || $carbon->isFuture())
|
||||
<em>@lang('server.files.seconds_ago')</em>
|
||||
@else
|
||||
{{ $carbon->diffForHumans() }}
|
||||
@endif
|
||||
</td>
|
||||
<td><button class="btn btn-xxs btn-default" data-action="toggleMenu" style="padding:2px 6px 0px;"><i class="fa fa-ellipsis-h"></i></button></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
Loading…
Reference in a new issue