Finalize service option import/export

This commit is contained in:
Dane Everitt 2017-10-03 23:31:04 -05:00
parent d608c313c3
commit 6269a08db7
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
16 changed files with 405 additions and 271 deletions

View file

@ -33,6 +33,16 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface
*/
public function getWithCopyAttributes(int $id): ServiceOption;
/**
* Return all of the data needed to export a service.
*
* @param int $id
* @return \Pterodactyl\Models\ServiceOption
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithExportAttributes(int $id): ServiceOption;
/**
* Confirm a copy script belongs to the same service as the item trying to use it.
*

View file

@ -10,7 +10,6 @@
namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\Service;
use Illuminate\Support\Collection;
interface ServiceRepositoryInterface extends RepositoryInterface
{
@ -18,23 +17,29 @@ interface ServiceRepositoryInterface extends RepositoryInterface
* Return a service or all services with their associated options, variables, and packs.
*
* @param int $id
* @return \Illuminate\Support\Collection
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithOptions(int $id = null): Collection;
public function getWithOptions(int $id = null);
/**
* Return a service or all services and the count of options, packs, and servers for that service.
*
* @param int|null $id
* @return \Illuminate\Support\Collection
* @return \Pterodactyl\Models\Service|\Illuminate\Database\Eloquent\Collection
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithCounts(int $id = null): Collection;
public function getWithCounts(int $id = null);
/**
* Return a service along with its associated options and the servers relation on those options.
*
* @param int $id
* @return mixed
* @return \Pterodactyl\Models\Service
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithOptionServers(int $id): Service;
}

View file

@ -0,0 +1,16 @@
<?php
/*
* 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
*/
namespace Pterodactyl\Exceptions\Service\ServiceOption;
use Pterodactyl\Exceptions\DisplayException;
class DuplicateOptionTagException extends DisplayException
{
}

View file

@ -9,26 +9,38 @@
namespace Pterodactyl\Http\Controllers\Admin\Services\Options;
use Illuminate\Http\RedirectResponse;
use Pterodactyl\Models\ServiceOption;
use Pterodactyl\Http\Controllers\Controller;
use Symfony\Component\HttpFoundation\Response;
use Pterodactyl\Services\Services\Exporter\XMLExporterService;
use Pterodactyl\Http\Requests\Admin\Service\OptionImportFormRequest;
use Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService;
use Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService;
class OptionShareController extends Controller
{
/**
* @var \Pterodactyl\Services\Services\Exporter\XMLExporterService
* @var \Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService
*/
protected $exporterService;
/**
* @var \Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService
*/
protected $importerService;
/**
* OptionShareController constructor.
*
* @param \Pterodactyl\Services\Services\Exporter\XMLExporterService $exporterService
* @param \Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService $exporterService
* @param \Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService $importerService
*/
public function __construct(XMLExporterService $exporterService)
{
public function __construct(
ServiceOptionExporterService $exporterService,
ServiceOptionImporterService $importerService
) {
$this->exporterService = $exporterService;
$this->importerService = $importerService;
}
/**
@ -42,8 +54,25 @@ class OptionShareController extends Controller
return response($this->exporterService->handle($option->id), 200, [
'Content-Transfer-Encoding' => 'binary',
'Content-Description' => 'File Transfer',
'Content-Disposition' => 'attachment; filename=' . kebab_case($option->name) . '.xml',
'Content-Type' => 'application/xml',
'Content-Disposition' => 'attachment; filename=' . kebab_case($option->name) . '.json',
'Content-Type' => 'application/json',
]);
}
/**
* Import a new service option using an XML file.
*
* @param \Pterodactyl\Http\Requests\Admin\Service\OptionImportFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException
*/
public function import(OptionImportFormRequest $request): RedirectResponse
{
$option = $this->importerService->handle($request->file('import_file'), $request->input('import_to_service'));
return redirect()->route('admin.services.option.view', ['option' => $option->id]);
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* 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
*/
namespace Pterodactyl\Http\Requests\Admin\Service;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class OptionImportFormRequest extends AdminFormRequest
{
/**
* @return array
*/
public function rules(): array
{
return [
'import_file' => 'bail|required|file|max:1000|mimetypes:application/json,text/plain',
'import_to_service' => 'bail|required|integer|exists:services,id',
];
}
}

View file

@ -65,7 +65,6 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract
*/
protected static $applicationRules = [
'service_id' => 'required',
'author' => 'required',
'name' => 'required',
'description' => 'required',
'tag' => 'required',
@ -83,10 +82,10 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract
*/
protected static $dataIntegrityRules = [
'service_id' => 'bail|numeric|exists:services,id',
'author' => 'email',
'uuid' => 'string|size:36',
'name' => 'string|max:255',
'description' => 'string',
'tag' => 'bail|alpha_num|max:60|unique:service_options,tag',
'tag' => 'bail|string|max:150',
'docker_image' => 'string|max:255',
'startup' => 'nullable|string',
'config_from' => 'bail|nullable|numeric|exists:service_options,id',

View file

@ -61,6 +61,25 @@ class ServiceOptionRepository extends EloquentRepository implements ServiceOptio
return $instance;
}
/**
* Return all of the data needed to export a service.
*
* @param int $id
* @return \Pterodactyl\Models\ServiceOption
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithExportAttributes(int $id): ServiceOption
{
/** @var \Pterodactyl\Models\ServiceOption $instance */
$instance = $this->getBuilder()->with('scriptFrom', 'configFrom', 'variables')->find($id, $this->getColumns());
if (! $instance) {
throw new RecordNotFoundException;
}
return $instance;
}
/**
* Confirm a copy script belongs to the same service as the item trying to use it.
*

View file

@ -10,7 +10,6 @@
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Service;
use Illuminate\Support\Collection;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface;
@ -25,9 +24,14 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI
}
/**
* {@inheritdoc}
* Return a service or all services with their associated options, variables, and packs.
*
* @param int $id
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithOptions(int $id = null): Collection
public function getWithOptions(int $id = null)
{
$instance = $this->getBuilder()->with('options.packs', 'options.variables');
@ -44,9 +48,14 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI
}
/**
* {@inheritdoc}
* Return a service or all services and the count of options, packs, and servers for that service.
*
* @param int|null $id
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithCounts(int $id = null): Collection
public function getWithCounts(int $id = null)
{
$instance = $this->getBuilder()->withCount(['options', 'packs', 'servers']);
@ -63,7 +72,12 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI
}
/**
* {@inheritdoc}
* Return a service along with its associated options and the servers relation on those options.
*
* @param int $id
* @return \Pterodactyl\Models\Service
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithOptionServers(int $id): Service
{

View file

@ -1,132 +0,0 @@
<?php
/**
* 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
*/
namespace Pterodactyl\Services\Services\Exporter;
use Closure;
use Carbon\Carbon;
use Sabre\Xml\Writer;
use Sabre\Xml\Service;
use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface;
class XMLExporterService
{
const XML_OPTION_NAMESPACE = '{https://pterodactyl.io/exporter/option/}';
/**
* @var \Carbon\Carbon
*/
protected $carbon;
/**
* @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface
*/
protected $repository;
/**
* @var \Sabre\Xml\Service
*/
protected $xml;
/**
* XMLExporterService constructor.
*
* @param \Carbon\Carbon $carbon
* @param \Sabre\Xml\Service $xml
* @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository
*/
public function __construct(
Carbon $carbon,
Service $xml,
ServiceOptionRepositoryInterface $repository
) {
$this->carbon = $carbon;
$this->repository = $repository;
$this->xml = $xml;
$this->xml->namespaceMap = [
str_replace(['{', '}'], '', self::XML_OPTION_NAMESPACE) => 'p',
];
}
/**
* Return an XML structure to represent this service option.
*
* @param int $option
* @return string
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(int $option): string
{
$option = $this->repository->getWithCopyAttributes($option);
$struct = [
'meta' => [
'version' => 'PTDL_v1',
],
'exported_at' => $this->carbon->now()->toIso8601String(),
'name' => $option->name,
'author' => array_get(explode(':', $option->tag), 0),
'tag' => $option->tag,
'description' => $this->writeCData($option->description),
'image' => $option->docker_image,
'config' => [
'files' => $this->writeCData($option->inherit_config_files),
'startup' => $this->writeCData($option->inherit_config_startup),
'logs' => $this->writeCData($option->inherit_config_logs),
'stop' => $option->inherit_config_stop,
],
'scripts' => [
'installation' => [
'script' => $this->writeCData($option->copy_script_install),
'container' => $option->copy_script_container,
'entrypoint' => $option->copy_script_entry,
],
],
];
return $this->xml->write(self::XML_OPTION_NAMESPACE . 'root', $this->recursiveArrayKeyPrepend($struct));
}
/**
* @param array $array
* @param string $prepend
*
* @return array
*/
protected function recursiveArrayKeyPrepend(array $array, $prepend = self::XML_OPTION_NAMESPACE): array
{
$parsed = [];
foreach ($array as $k => &$v) {
$k = $prepend . $k;
if (is_array($v)) {
$v = $this->recursiveArrayKeyPrepend($v);
}
$parsed[$k] = $v;
}
return $parsed;
}
/**
* Return a closure to be used by the XML writer to generate a string wrapped in CDATA tags.
*
* @param string $value
* @return \Closure
*/
protected function writeCData(string $value): Closure
{
return function (Writer $writer) use ($value) {
return $writer->writeCData($value);
};
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* 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
*/
namespace Pterodactyl\Services\Services\Sharing;
use Carbon\Carbon;
use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface;
class ServiceOptionExporterService
{
/**
* @var \Carbon\Carbon
*/
protected $carbon;
/**
* @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface
*/
protected $repository;
/**
* XMLExporterService constructor.
*
* @param \Carbon\Carbon $carbon
* @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository
*/
public function __construct(
Carbon $carbon,
ServiceOptionRepositoryInterface $repository
) {
$this->carbon = $carbon;
$this->repository = $repository;
}
/**
* Return an XML structure to represent this service option.
*
* @param int $option
* @return string
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(int $option): string
{
$option = $this->repository->getWithExportAttributes($option);
$struct = [
'_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO',
'meta' => [
'version' => 'PTDL_v1',
],
'exported_at' => $this->carbon->now()->toIso8601String(),
'name' => $option->name,
'author' => array_get(explode(':', $option->tag), 0),
'tag' => $option->tag,
'description' => $option->description,
'image' => $option->docker_image,
'startup' => $option->display_startup,
'config' => [
'files' => $option->inherit_config_files,
'startup' => $option->inherit_config_startup,
'logs' => $option->inherit_config_logs,
'stop' => $option->inherit_config_stop,
],
'scripts' => [
'installation' => [
'script' => $option->copy_script_install,
'container' => $option->copy_script_container,
'entrypoint' => $option->copy_script_entry,
],
],
'variables' => $option->variables->transform(function ($item) {
return collect($item->toArray())->except([
'id', 'option_id', 'created_at', 'updated_at',
])->toArray();
}),
];
return json_encode($struct, JSON_PRETTY_PRINT);
}
}

View file

@ -0,0 +1,123 @@
<?php
/**
* 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
*/
namespace Pterodactyl\Services\Services\Sharing;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\UploadedFile;
use Pterodactyl\Models\ServiceOption;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface;
use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException;
use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface;
use Pterodactyl\Exceptions\Service\ServiceOption\DuplicateOptionTagException;
class ServiceOptionImporterService
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface
*/
protected $serviceRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface
*/
protected $serviceVariableRepository;
/**
* XMLImporterService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository
* @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface $serviceVariableRepository
*/
public function __construct(
ConnectionInterface $connection,
ServiceRepositoryInterface $serviceRepository,
ServiceOptionRepositoryInterface $repository,
ServiceVariableRepositoryInterface $serviceVariableRepository
) {
$this->connection = $connection;
$this->repository = $repository;
$this->serviceRepository = $serviceRepository;
$this->serviceVariableRepository = $serviceVariableRepository;
}
/**
* Take an uploaded XML file and parse it into a new service option.
*
* @param \Illuminate\Http\UploadedFile $file
* @param int $service
* @return \Pterodactyl\Models\ServiceOption
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException
*/
public function handle(UploadedFile $file, int $service): ServiceOption
{
if (! $file->isValid() || ! $file->isFile()) {
throw new InvalidFileUploadException(trans('exceptions.service.exporter.import_file_error'));
}
$parsed = json_decode($file->openFile()->fread($file->getSize()));
if (object_get($parsed, 'meta.version') !== 'PTDL_v1') {
throw new InvalidFileUploadException(trans('exceptions.service.exporter.invalid_json_provided'));
}
$service = $this->serviceRepository->getWithOptions($service);
$service->options->each(function ($option) use ($parsed) {
if ($option->tag === object_get($parsed, 'tag')) {
throw new DuplicateOptionTagException(trans('exceptions.service.options.duplicate_tag'));
}
});
$this->connection->beginTransaction();
$option = $this->repository->create([
'uuid' => Uuid::uuid4()->toString(),
'service_id' => $service->id,
'name' => object_get($parsed, 'name'),
'description' => object_get($parsed, 'description'),
'tag' => object_get($parsed, 'tag'),
'docker_image' => object_get($parsed, 'image'),
'config_files' => object_get($parsed, 'config.files'),
'config_startup' => object_get($parsed, 'config.startup'),
'config_logs' => object_get($parsed, 'config.logs'),
'config_stop' => object_get($parsed, 'config.stop'),
'startup' => object_get($parsed, 'startup'),
'script_install' => object_get($parsed, 'scripts.installation.script'),
'script_entry' => object_get($parsed, 'scripts.installation.entrypoint'),
'script_container' => object_get($parsed, 'scripts.installation.container'),
'copy_script_from' => null,
], true, true);
collect($parsed->variables)->each(function ($variable) use ($option) {
$this->serviceVariableRepository->create(array_merge((array) $variable, [
'option_id' => $option->id,
]));
});
$this->connection->commit();
return $option;
}
}

View file

@ -35,7 +35,6 @@
"prologue/alerts": "^0.4",
"ramsey/uuid": "^3.7",
"s1lentium/iptools": "^1.1",
"sabre/xml": "^2.0",
"sofa/eloquence": "~5.4.1",
"spatie/laravel-fractal": "^4.0",
"watson/validating": "^3.0",

116
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "bc8c88f86ea043406bce2f8fddf704b3",
"content-hash": "46a0a06ec8f3af50ed6ec05c2bb3b9a3",
"packages": [
{
"name": "aws/aws-sdk-php",
@ -2510,120 +2510,6 @@
],
"time": "2016-08-21T15:57:09+00:00"
},
{
"name": "sabre/uri",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/fruux/sabre-uri.git",
"reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruux/sabre-uri/zipball/a42126042c7dcb53e2978dadb6d22574d1359b4c",
"reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c",
"shasum": ""
},
"require": {
"php": ">=7"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"sabre/cs": "~1.0.0"
},
"type": "library",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\Uri\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "Functions for making sense out of URIs.",
"homepage": "http://sabre.io/uri/",
"keywords": [
"rfc3986",
"uri",
"url"
],
"time": "2017-02-20T20:02:35+00:00"
},
{
"name": "sabre/xml",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/fruux/sabre-xml.git",
"reference": "054292959a1f2b64c10c9c7a03a816ba1872b8a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruux/sabre-xml/zipball/054292959a1f2b64c10c9c7a03a816ba1872b8a3",
"reference": "054292959a1f2b64c10c9c7a03a816ba1872b8a3",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"lib-libxml": ">=2.6.20",
"php": ">=7.0",
"sabre/uri": ">=1.0,<3.0.0"
},
"require-dev": {
"phpunit/phpunit": "*",
"sabre/cs": "~1.0.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Sabre\\Xml\\": "lib/"
},
"files": [
"lib/Deserializer/functions.php",
"lib/Serializer/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Markus Staab",
"email": "markus.staab@redaxo.de",
"role": "Developer"
}
],
"description": "sabre/xml is an XML library that you may not hate.",
"homepage": "https://sabre.io/xml/",
"keywords": [
"XMLReader",
"XMLWriter",
"dom",
"xml"
],
"time": "2016-11-16T00:41:01+00:00"
},
{
"name": "sofa/eloquence",
"version": "5.4.1",

View file

@ -21,6 +21,7 @@ return [
'service' => [
'delete_has_servers' => 'A service with active servers attached to it cannot be deleted from the Panel.',
'options' => [
'duplicate_tag' => 'A service option with that tag already exists for this service.',
'delete_has_servers' => 'A service option with active servers attached to it cannot be deleted from the Panel.',
'invalid_copy_id' => 'The service option selected for copying a script from either does not exist, or is copying a script itself.',
'must_be_child' => 'The "Copy Settings From" directive for this option must be a child option for the selected service.',
@ -30,6 +31,10 @@ return [
'env_not_unique' => 'The environment variable :name must be unique to this service option.',
'reserved_name' => 'The environment variable :name is protected and cannot be assigned to a variable.',
],
'exporter' => [
'import_file_error' => 'The XML file provided was not valid.',
'invalid_json_provided' => 'The JSON file provided is not in a format that can be recognized.',
],
],
'packs' => [
'delete_has_servers' => 'Cannot delete a pack that is attached to active servers.',

View file

@ -31,7 +31,8 @@
<div class="box-header with-border">
<h3 class="box-title">Configured Service</h3>
<div class="box-tools">
<a href="{{ route('admin.services.new') }}"><button class="btn btn-primary btn-sm">Create New</button></a>
<a href="#" class="btn btn-sm btn-success" data-toggle="modal" data-target="#importServiceOptionModal" role="button"><i class="fa fa-upload"></i> Import Service Option</a>
<a href="{{ route('admin.services.new') }}" class="btn btn-primary btn-sm">Create New</a>
</div>
</div>
<div class="box-body table-responsive no-padding">
@ -57,4 +58,50 @@
</div>
</div>
</div>
<div class="modal fade" tabindex="-1" role="dialog" id="importServiceOptionModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Import a Service Option</h4>
</div>
<form action="{{ route('admin.services.option.import') }}" enctype="multipart/form-data" method="POST">
<div class="modal-body">
<div class="form-group">
<label class="control-label" for="pImportFile">Service File <span class="field-required"></span></label>
<div>
<input id="pImportFile" type="file" name="import_file" class="form-control" accept="application/json" />
<p class="small text-muted">Select the <code>.json</code> file for the new service option that you wish to import.</p>
</div>
</div>
<div class="form-group">
<label class="control-label" for="pImportToService">Associated Service <span class="field-required"></span></label>
<div>
<select id="pImportToService" name="import_to_service">
@foreach($services as $service)
<option value="{{ $service->id }}">{{ $service->name }} &lt;{{ $service->author }}&gt;</option>
@endforeach
</select>
<p class="small text-muted">Select the service that this option will be associated with from the dropdown. If you wish to associate it with a new service you will need to create that service before continuing.</p>
</div>
</div>
</div>
<div class="modal-footer">
{{ csrf_field() }}
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Import</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
$(document).ready(function() {
$('#pImportToService').select2();
});
</script>
@endsection

View file

@ -160,6 +160,7 @@ Route::group(['prefix' => 'services'], function () {
Route::get('/option/{option}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts');
Route::post('/new', 'ServiceController@store');
Route::post('/import', 'Services\Options\OptionShareController@import')->name('admin.services.option.import');
Route::post('/option/new', 'OptionController@store');
Route::post('/option/{option}/variables', 'VariableController@store');