mirror of
https://github.com/pterodactyl/panel.git
synced 2024-11-25 10:32:31 +01:00
Merge branch 'develop' into feature/server-mounts
This commit is contained in:
commit
295f09ca43
12
.babel-plugin-macrosrc.js
Normal file
12
.babel-plugin-macrosrc.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
twin: {
|
||||
preset: 'styled-components',
|
||||
autoCssProp: true,
|
||||
config: './tailwind.config.js',
|
||||
},
|
||||
styledComponents: {
|
||||
pure: true,
|
||||
displayName: false,
|
||||
fileName: false,
|
||||
},
|
||||
};
|
87
.github/workflows/release.yml
vendored
Normal file
87
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
name: "Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
|
||||
- name: Create release branch and bump version
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
BRANCH=release/${REF:10}
|
||||
git config --local user.email "ci@pterodactyl.io"
|
||||
git config --local user.name "Pterodactyl CI"
|
||||
git checkout -b $BRANCH
|
||||
git push -u origin $BRANCH
|
||||
sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:11}',/" config/app.php
|
||||
git add config/app.php
|
||||
git commit -m "bump version for release"
|
||||
git push
|
||||
|
||||
- name: Build assets
|
||||
run: |
|
||||
yarn install
|
||||
yarn run build:production
|
||||
|
||||
- name: Create release archive
|
||||
run: |
|
||||
rm -rf node_modules/ test/ codecov.yml CODE_OF_CONDUCT.md CONTRIBUTING.md phpunit.dusk.xml phpunit.xml Vagrantfile
|
||||
tar -czf panel.tar.gz *
|
||||
|
||||
- name: Extract changelog
|
||||
id: extract_changelog
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
|
||||
echo ::set-output name=version_name::`sed -nr "s/^## (${REF:10} .*)$/\1/p" CHANGELOG.md`
|
||||
|
||||
- name: Create checksum and add to changelog
|
||||
run: |
|
||||
SUM=`sha256sum panel.tar.gz`
|
||||
echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG
|
||||
echo $SUM > checksum.txt
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ steps.extract_changelog.outputs.version_name }}
|
||||
body_path: ./RELEASE_CHANGELOG
|
||||
draft: true
|
||||
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||
|
||||
- name: Upload binary
|
||||
id: upload-release-archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: panel.tar.gz
|
||||
asset_name: panel.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
|
||||
- name: Upload checksum
|
||||
id: upload-release-checksum
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./checksum.txt
|
||||
asset_name: checksum.txt
|
||||
asset_content_type: text/plain
|
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@ -1,6 +1,9 @@
|
||||
name: tests
|
||||
on:
|
||||
push:
|
||||
branch-ignore:
|
||||
- 'master'
|
||||
- 'release/**'
|
||||
pull_request:
|
||||
jobs:
|
||||
integration_tests:
|
||||
|
@ -3,36 +3,9 @@
|
||||
namespace Pterodactyl\Contracts\Repository;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
interface AllocationRepositoryInterface extends RepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Set an array of allocation IDs to be assigned to a specific server.
|
||||
*
|
||||
* @param int|null $server
|
||||
* @param array $ids
|
||||
* @return int
|
||||
*/
|
||||
public function assignAllocationsToServer(int $server = null, array $ids): int;
|
||||
|
||||
/**
|
||||
* Return all of the allocations for a specific node.
|
||||
*
|
||||
* @param int $node
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function getAllocationsForNode(int $node): Collection;
|
||||
|
||||
/**
|
||||
* Return all of the allocations for a node in a paginated format.
|
||||
*
|
||||
* @param int $node
|
||||
* @param int $perPage
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function getPaginatedAllocationsForNode(int $node, int $perPage = 100): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Return all of the unique IPs that exist for a given node.
|
||||
*
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace Pterodactyl\Contracts\Repository;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
@ -107,16 +106,6 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
|
||||
*/
|
||||
public function getDaemonServiceData(Server $server, bool $refresh = false): array;
|
||||
|
||||
/**
|
||||
* Return a paginated list of servers that a user can access at a given level.
|
||||
*
|
||||
* @param \Pterodactyl\Models\User $user
|
||||
* @param int $level
|
||||
* @param bool|int $paginate
|
||||
* @return \Illuminate\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function filterUserAccessServers(User $user, int $level, $paginate = 25);
|
||||
|
||||
/**
|
||||
* Return a server by UUID.
|
||||
*
|
||||
|
@ -3,11 +3,10 @@
|
||||
namespace Pterodactyl\Http\Controllers\Api\Application\Nodes;
|
||||
|
||||
use Pterodactyl\Models\Node;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Pterodactyl\Services\Allocations\AssignmentService;
|
||||
use Pterodactyl\Services\Allocations\AllocationDeletionService;
|
||||
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
|
||||
use Pterodactyl\Transformers\Api\Application\AllocationTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest;
|
||||
@ -26,41 +25,32 @@ class AllocationController extends ApplicationApiController
|
||||
*/
|
||||
private $deletionService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* AllocationController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService
|
||||
* @param \Pterodactyl\Services\Allocations\AllocationDeletionService $deletionService
|
||||
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(
|
||||
AssignmentService $assignmentService,
|
||||
AllocationDeletionService $deletionService,
|
||||
AllocationRepositoryInterface $repository
|
||||
AllocationDeletionService $deletionService
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->assignmentService = $assignmentService;
|
||||
$this->deletionService = $deletionService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of the allocations that exist for a given node.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest $request
|
||||
* @param \Pterodactyl\Models\Node $node
|
||||
* @return array
|
||||
*/
|
||||
public function index(GetAllocationsRequest $request): array
|
||||
public function index(GetAllocationsRequest $request, Node $node): array
|
||||
{
|
||||
$allocations = $this->repository->getPaginatedAllocationsForNode(
|
||||
$request->getModel(Node::class)->id, 50
|
||||
);
|
||||
$allocations = $node->allocations()->paginate(50);
|
||||
|
||||
return $this->fractal->collection($allocations)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
@ -71,32 +61,35 @@ class AllocationController extends ApplicationApiController
|
||||
* Store new allocations for a given node.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Application\Allocations\StoreAllocationRequest $request
|
||||
* @return \Illuminate\Http\Response
|
||||
* @param \Pterodactyl\Models\Node $node
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
public function store(StoreAllocationRequest $request): Response
|
||||
public function store(StoreAllocationRequest $request, Node $node): JsonResponse
|
||||
{
|
||||
$this->assignmentService->handle($request->getModel(Node::class), $request->validated());
|
||||
$this->assignmentService->handle($node, $request->validated());
|
||||
|
||||
return response('', 204);
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific allocation from the Panel.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest $request
|
||||
* @return \Illuminate\Http\Response
|
||||
* @param \Pterodactyl\Models\Node $node
|
||||
* @param \Pterodactyl\Models\Allocation $allocation
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException
|
||||
*/
|
||||
public function delete(DeleteAllocationRequest $request): Response
|
||||
public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): JsonResponse
|
||||
{
|
||||
$this->deletionService->handle($request->getModel(Allocation::class));
|
||||
$this->deletionService->handle($allocation);
|
||||
|
||||
return response('', 204);
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,40 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
|
||||
abstract class ClientApiController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* Returns only the includes which are valid for the given transformer.
|
||||
*
|
||||
* @param \Pterodactyl\Transformers\Api\Client\BaseClientTransformer $transformer
|
||||
* @param array $merge
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getIncludesForTransformer(BaseClientTransformer $transformer, array $merge = [])
|
||||
{
|
||||
$filtered = array_filter($this->parseIncludes(), function ($datum) use ($transformer) {
|
||||
return in_array($datum, $transformer->getAvailableIncludes());
|
||||
});
|
||||
|
||||
return array_merge($filtered, $merge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parsed includes for this request.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function parseIncludes()
|
||||
{
|
||||
$includes = $this->request->query('include') ?? [];
|
||||
|
||||
if (! is_string($includes)) {
|
||||
return $includes;
|
||||
}
|
||||
|
||||
return array_map(function ($item) {
|
||||
return trim($item);
|
||||
}, explode(',', $includes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of an application transformer.
|
||||
*
|
||||
|
@ -3,7 +3,9 @@
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
|
||||
use Pterodactyl\Http\Requests\Api\Client\GetServersRequest;
|
||||
@ -36,32 +38,36 @@ class ClientController extends ClientApiController
|
||||
*/
|
||||
public function index(GetServersRequest $request): array
|
||||
{
|
||||
// Check for the filter parameter on the request.
|
||||
switch ($request->input('filter')) {
|
||||
case 'all':
|
||||
$filter = User::FILTER_LEVEL_ALL;
|
||||
break;
|
||||
case 'admin':
|
||||
$filter = User::FILTER_LEVEL_ADMIN;
|
||||
break;
|
||||
case 'owner':
|
||||
$filter = User::FILTER_LEVEL_OWNER;
|
||||
break;
|
||||
case 'subuser-of':
|
||||
default:
|
||||
$filter = User::FILTER_LEVEL_SUBUSER;
|
||||
break;
|
||||
$user = $request->user();
|
||||
$level = $request->getFilterLevel();
|
||||
$transformer = $this->getTransformer(ServerTransformer::class);
|
||||
|
||||
// Start the query builder and ensure we eager load any requested relationships from the request.
|
||||
$builder = Server::query()->with($this->getIncludesForTransformer($transformer, ['node']));
|
||||
|
||||
if ($level === User::FILTER_LEVEL_OWNER) {
|
||||
$builder = $builder->where('owner_id', $request->user()->id);
|
||||
}
|
||||
// If set to all, display all servers they can access, including those they access as an
|
||||
// admin. If set to subuser, only return the servers they can access because they are owner,
|
||||
// or marked as a subuser of the server.
|
||||
elseif (($level === User::FILTER_LEVEL_ALL && ! $user->root_admin) || $level === User::FILTER_LEVEL_SUBUSER) {
|
||||
$builder = $builder->whereIn('id', $user->accessibleServers()->pluck('id')->all());
|
||||
}
|
||||
// If set to admin, only display the servers a user can access because they are an administrator.
|
||||
// This means only servers the user would not have access to if they were not an admin (because they
|
||||
// are not an owner or subuser) are returned.
|
||||
elseif ($level === User::FILTER_LEVEL_ADMIN && $user->root_admin) {
|
||||
$builder = $builder->whereNotIn('id', $user->accessibleServers()->pluck('id')->all());
|
||||
}
|
||||
|
||||
$servers = $this->repository
|
||||
->setSearchTerm($request->input('query'))
|
||||
->filterUserAccessServers(
|
||||
$request->user(), $filter, config('pterodactyl.paginate.frontend.servers')
|
||||
);
|
||||
$builder = QueryBuilder::for($builder)->allowedFilters(
|
||||
'uuid', 'name', 'external_id'
|
||||
);
|
||||
|
||||
return $this->fractal->collection($servers)
|
||||
->transformWith($this->getTransformer(ServerTransformer::class))
|
||||
->toArray();
|
||||
$servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query());
|
||||
|
||||
return $this->fractal->transformWith($transformer)->collection($servers)->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,7 +159,7 @@ class FileController extends ClientApiController
|
||||
{
|
||||
$this->fileRepository
|
||||
->setServer($server)
|
||||
->createDirectory($request->input('name'), $request->input('directory', '/'));
|
||||
->createDirectory($request->input('name'), $request->input('root', '/'));
|
||||
|
||||
return Response::create('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
|
||||
|
||||
class NetworkAllocationController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\AllocationRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
||||
*/
|
||||
private $serverRepository;
|
||||
|
||||
/**
|
||||
* NetworkController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $repository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $serverRepository
|
||||
*/
|
||||
public function __construct(
|
||||
AllocationRepository $repository,
|
||||
ServerRepository $serverRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->serverRepository = $serverRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all of the allocations available to a server and wether or
|
||||
* not they are currently assigned as the primary for this server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
*/
|
||||
public function index(GetNetworkRequest $request, Server $server): array
|
||||
{
|
||||
return $this->fractal->collection($server->allocations)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Allocation $allocation
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(UpdateAllocationRequest $request, Server $server, Allocation $allocation): array
|
||||
{
|
||||
$allocation = $this->repository->update($allocation->id, [
|
||||
'notes' => $request->input('notes'),
|
||||
]);
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Allocation $allocation
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, Allocation $allocation): array
|
||||
{
|
||||
$this->serverRepository->update($server->id, ['allocation_id' => $allocation->id]);
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an allocation from a server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Allocation $allocation
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation)
|
||||
{
|
||||
if ($allocation->id === $server->allocation_id) {
|
||||
throw new DisplayException(
|
||||
'Cannot delete the primary allocation for a server.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->repository->update($allocation->id, ['server_id' => null, 'notes' => null]);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
|
||||
|
||||
class NetworkController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\AllocationRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* NetworkController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $repository
|
||||
*/
|
||||
public function __construct(AllocationRepository $repository)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all of the allocations available to a server and wether or
|
||||
* not they are currently assigned as the primary for this server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
*/
|
||||
public function index(GetNetworkRequest $request, Server $server): array
|
||||
{
|
||||
$allocations = $this->repository->findWhere([
|
||||
['server_id', '=', $server->id],
|
||||
]);
|
||||
|
||||
return $this->fractal->collection($allocations)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
}
|
@ -2,8 +2,6 @@
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Base;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
|
||||
@ -27,15 +25,10 @@ class IndexController extends Controller
|
||||
/**
|
||||
* Returns listing of user's servers.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function index(Request $request)
|
||||
public function index()
|
||||
{
|
||||
$servers = $this->repository->setSearchTerm($request->input('query'))->filterUserAccessServers(
|
||||
$request->user(), User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers')
|
||||
);
|
||||
|
||||
return view('templates/base.core', ['servers' => $servers]);
|
||||
return view('templates/base.core');
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Api\Client\Server;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class AllocationBelongsToServer
|
||||
{
|
||||
/**
|
||||
* Ensure that the allocation found in the URL belongs to the server being queried.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
$server = $request->route()->parameter('server');
|
||||
/** @var \Pterodactyl\Models\Allocation|null $allocation */
|
||||
$allocation = $request->route()->parameter('allocation');
|
||||
|
||||
if ($allocation && $allocation->server_id !== $server->id) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -65,7 +65,7 @@ class AuthenticateServerAccess
|
||||
}
|
||||
|
||||
if ($server->suspended) {
|
||||
throw new AccessDeniedHttpException('This server is currenty suspended and the functionality requested is unavailable.');
|
||||
throw new AccessDeniedHttpException('This server is currently suspended and the functionality requested is unavailable.');
|
||||
}
|
||||
|
||||
if (! $server->isInstalled()) {
|
||||
|
@ -4,12 +4,12 @@ namespace Pterodactyl\Http\Middleware\Api\Client;
|
||||
|
||||
use Closure;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Database;
|
||||
use Illuminate\Container\Container;
|
||||
use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
||||
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
||||
|
||||
class SubstituteClientApiBindings extends ApiSubstituteBindings
|
||||
{
|
||||
@ -43,17 +43,9 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
|
||||
});
|
||||
|
||||
$this->router->bind('database', function ($value) use ($request) {
|
||||
try {
|
||||
$id = Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value);
|
||||
$id = Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value);
|
||||
|
||||
return Container::getInstance()->make(DatabaseRepositoryInterface::class)->findFirstWhere([
|
||||
['id', '=', $id],
|
||||
]);
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
$request->attributes->set('is_missing_model', true);
|
||||
|
||||
return null;
|
||||
}
|
||||
return Database::query()->where('id', $id)->firstOrFail();
|
||||
});
|
||||
|
||||
$this->router->model('backup', Backup::class, function ($value) {
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
|
||||
class GetServersRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
@ -11,4 +13,28 @@ class GetServersRequest extends ClientApiRequest
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filtering method for servers when the client base endpoint is requested.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getFilterLevel(): int
|
||||
{
|
||||
switch ($this->input('type')) {
|
||||
case 'all':
|
||||
return User::FILTER_LEVEL_ALL;
|
||||
break;
|
||||
case 'admin':
|
||||
return User::FILTER_LEVEL_ADMIN;
|
||||
break;
|
||||
case 'owner':
|
||||
return User::FILTER_LEVEL_OWNER;
|
||||
break;
|
||||
case 'subuser-of':
|
||||
default:
|
||||
return User::FILTER_LEVEL_SUBUSER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Network;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class DeleteAllocationRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_ALLOCATION_DELETE;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Network;
|
||||
|
||||
class SetPrimaryAllocationRequest extends UpdateAllocationRequest
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Network;
|
||||
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class UpdateAllocationRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_ALLOCATION_UPDATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = Allocation::getRules();
|
||||
|
||||
return [
|
||||
'notes' => array_merge($rules['notes'], ['present']),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\ViewComposers\Server;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ServerDataComposer
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Http\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* ServerDataComposer constructor.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach server data to a view automatically.
|
||||
*
|
||||
* @param \Illuminate\View\View $view
|
||||
*/
|
||||
public function compose(View $view)
|
||||
{
|
||||
$server = $this->request->get('server');
|
||||
|
||||
$view->with('server', $server);
|
||||
$view->with('node', object_get($server, 'node'));
|
||||
$view->with('daemon_token', $this->request->get('server_token'));
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\ViewComposers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
|
||||
class ServerListComposer
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Http\Request
|
||||
*/
|
||||
private $request;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* ServerListComposer constructor.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(Request $request, ServerRepositoryInterface $repository)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a list of servers the user can access to the view.
|
||||
*
|
||||
* @param \Illuminate\View\View $view
|
||||
*/
|
||||
public function compose(View $view)
|
||||
{
|
||||
if (! $this->request->user()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$servers = $this->repository
|
||||
->setColumns(['id', 'owner_id', 'uuidShort', 'name', 'description'])
|
||||
->filterUserAccessServers($this->request->user(), User::FILTER_LEVEL_SUBUSER, false);
|
||||
|
||||
$view->with('sidebarServerList', $servers);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ namespace Pterodactyl\Models;
|
||||
* @property string|null $ip_alias
|
||||
* @property int $port
|
||||
* @property int|null $server_id
|
||||
* @property string|null $notes
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
*
|
||||
@ -60,6 +61,7 @@ class Allocation extends Model
|
||||
'port' => 'required|numeric|between:1024,65553',
|
||||
'ip_alias' => 'nullable|string',
|
||||
'server_id' => 'nullable|exists:servers,id',
|
||||
'notes' => 'nullable|string|max:256',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -44,7 +44,9 @@ class Permission extends Model
|
||||
const ACTION_BACKUP_DOWNLOAD = 'backup.download';
|
||||
|
||||
const ACTION_ALLOCATION_READ = 'allocation.read';
|
||||
const ACTION_ALLOCIATION_UPDATE = 'allocation.update';
|
||||
const ACTION_ALLOCATION_CREATE = 'allocation.create';
|
||||
const ACTION_ALLOCATION_UPDATE = 'allocation.update';
|
||||
const ACTION_ALLOCATION_DELETE = 'allocation.delete';
|
||||
|
||||
const ACTION_FILE_READ = 'file.read';
|
||||
const ACTION_FILE_CREATE = 'file.create';
|
||||
@ -157,7 +159,9 @@ class Permission extends Model
|
||||
'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view the allocations assigned to this server.',
|
||||
'update' => 'Allows a user to modify the allocations assigned to this server.',
|
||||
'create' => 'Allows a user to assign additional allocations to the server.',
|
||||
'update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.',
|
||||
'delete' => 'Allows a user to delete an allocation from the server.',
|
||||
],
|
||||
],
|
||||
|
||||
|
@ -17,6 +17,11 @@ class RecoveryToken extends Model
|
||||
*/
|
||||
const UPDATED_AT = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
|
@ -7,6 +7,7 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\Rules\In;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Pterodactyl\Models\Traits\Searchable;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Pterodactyl\Traits\Helpers\AvailableLanguages;
|
||||
@ -260,4 +261,21 @@ class User extends Model implements
|
||||
{
|
||||
return $this->hasMany(RecoveryToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the servers that a user can access by way of being the owner of the
|
||||
* server, or because they are assigned as a subuser for that server.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function accessibleServers()
|
||||
{
|
||||
return Server::query()
|
||||
->select('servers.*')
|
||||
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
|
||||
->where(function (Builder $builder) {
|
||||
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id);
|
||||
})
|
||||
->groupBy('servers.id');
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ namespace Pterodactyl\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Pterodactyl\Http\ViewComposers\AssetComposer;
|
||||
use Pterodactyl\Http\ViewComposers\ServerListComposer;
|
||||
use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer;
|
||||
|
||||
class ViewComposerServiceProvider extends ServiceProvider
|
||||
{
|
||||
@ -15,10 +13,5 @@ class ViewComposerServiceProvider extends ServiceProvider
|
||||
public function boot()
|
||||
{
|
||||
$this->app->make('view')->composer('*', AssetComposer::class);
|
||||
|
||||
$this->app->make('view')->composer('server.*', ServerDataComposer::class);
|
||||
|
||||
// Add data to make the sidebar work when viewing a server.
|
||||
$this->app->make('view')->composer(['server.*'], ServerListComposer::class);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ namespace Pterodactyl\Repositories\Eloquent;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
|
||||
|
||||
class AllocationRepository extends EloquentRepository implements AllocationRepositoryInterface
|
||||
@ -20,41 +19,6 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
|
||||
return Allocation::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an array of allocation IDs to be assigned to a specific server.
|
||||
*
|
||||
* @param int|null $server
|
||||
* @param array $ids
|
||||
* @return int
|
||||
*/
|
||||
public function assignAllocationsToServer(int $server = null, array $ids): int
|
||||
{
|
||||
return $this->getBuilder()->whereIn('id', $ids)->update(['server_id' => $server]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of the allocations for a specific node.
|
||||
*
|
||||
* @param int $node
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function getAllocationsForNode(int $node): Collection
|
||||
{
|
||||
return $this->getBuilder()->where('node_id', $node)->get($this->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of the allocations for a node in a paginated format.
|
||||
*
|
||||
* @param int $node
|
||||
* @param int $perPage
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function getPaginatedAllocationsForNode(int $node, int $perPage = 100): LengthAwarePaginator
|
||||
{
|
||||
return $this->getBuilder()->where('node_id', $node)->paginate($perPage, $this->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of the unique IPs that exist for a given node.
|
||||
*
|
||||
|
@ -2,9 +2,11 @@
|
||||
|
||||
namespace Pterodactyl\Repositories\Eloquent;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Repositories\Repository;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
@ -15,6 +17,53 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
|
||||
|
||||
abstract class EloquentRepository extends Repository implements RepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $useRequestFilters = false;
|
||||
|
||||
/**
|
||||
* Determines if the repository function should use filters off the request object
|
||||
* present when returning results. This allows repository methods to be called in API
|
||||
* context's such that we can pass through ?filter[name]=Dane&sort=desc for example.
|
||||
*
|
||||
* @param bool $usingFilters
|
||||
* @return $this
|
||||
*/
|
||||
public function usingRequestFilters($usingFilters = true)
|
||||
{
|
||||
$this->useRequestFilters = $usingFilters;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request instance.
|
||||
*
|
||||
* @return \Illuminate\Http\Request
|
||||
*/
|
||||
protected function request()
|
||||
{
|
||||
return $this->app->make(Request::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate the response data based on the page para.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $instance
|
||||
* @param int $default
|
||||
*
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
protected function paginate(Builder $instance, int $default = 50)
|
||||
{
|
||||
if (! $this->useRequestFilters) {
|
||||
return $instance->paginate($default);
|
||||
}
|
||||
|
||||
return $instance->paginate($this->request()->query('per_page', $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the eloquent model bound to this
|
||||
* repository instance.
|
||||
@ -236,6 +285,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
|
||||
* Return all records associated with the given model.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
* @deprecated Just use the model
|
||||
*/
|
||||
public function all(): Collection
|
||||
{
|
||||
@ -313,6 +363,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
|
||||
* Get the amount of entries in the database.
|
||||
*
|
||||
* @return int
|
||||
* @deprecated just use the count method off a model
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace Pterodactyl\Repositories\Eloquent;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@ -226,43 +225,6 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a paginated list of servers that a user can access at a given level.
|
||||
*
|
||||
* @param \Pterodactyl\Models\User $user
|
||||
* @param int $level
|
||||
* @param bool|int $paginate
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function filterUserAccessServers(User $user, int $level, $paginate = 25)
|
||||
{
|
||||
$instance = $this->getBuilder()->select($this->getColumns())->with(['user', 'node', 'allocation']);
|
||||
|
||||
// If access level is set to owner, only display servers
|
||||
// that the user owns.
|
||||
if ($level === User::FILTER_LEVEL_OWNER) {
|
||||
$instance->where('owner_id', $user->id);
|
||||
}
|
||||
|
||||
// If set to all, display all servers they can access, including
|
||||
// those they access as an admin. If set to subuser, only return
|
||||
// the servers they can access because they are owner, or marked
|
||||
// as a subuser of the server.
|
||||
elseif (($level === User::FILTER_LEVEL_ALL && ! $user->root_admin) || $level === User::FILTER_LEVEL_SUBUSER) {
|
||||
$instance->whereIn('id', $this->getUserAccessServers($user->id));
|
||||
}
|
||||
|
||||
// If set to admin, only display the servers a user can access
|
||||
// as an administrator (leaves out owned and subuser of).
|
||||
elseif ($level === User::FILTER_LEVEL_ADMIN && $user->root_admin) {
|
||||
$instance->whereNotIn('id', $this->getUserAccessServers($user->id));
|
||||
}
|
||||
|
||||
$instance->search($this->getSearchTerm());
|
||||
|
||||
return $paginate ? $instance->paginate($paginate) : $instance->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a server by UUID.
|
||||
*
|
||||
@ -339,20 +301,6 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
|
||||
return ! $this->getBuilder()->where('uuid', '=', $uuid)->orWhere('uuidShort', '=', $short)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of server IDs that a given user can access based
|
||||
* on owner and subuser permissions.
|
||||
*
|
||||
* @param int $user
|
||||
* @return int[]
|
||||
*/
|
||||
private function getUserAccessServers(int $user): array
|
||||
{
|
||||
return $this->getBuilder()->select('id')->where('owner_id', $user)->union(
|
||||
$this->app->make(SubuserRepository::class)->getBuilder()->select('server_id')->where('user_id', $user)
|
||||
)->pluck('id')->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of servers that are suspended.
|
||||
*
|
||||
|
@ -270,7 +270,9 @@ class ServerCreationService
|
||||
$records = array_merge($records, $data['allocation_additional']);
|
||||
}
|
||||
|
||||
$this->allocationRepository->assignAllocationsToServer($server->id, $records);
|
||||
$this->allocationRepository->updateWhereIn('id', $records, [
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,6 +39,7 @@ class AllocationTransformer extends BaseTransformer
|
||||
'ip' => $allocation->ip,
|
||||
'alias' => $allocation->ip_alias,
|
||||
'port' => $allocation->port,
|
||||
'notes' => $allocation->notes,
|
||||
'assigned' => ! is_null($allocation->server_id),
|
||||
];
|
||||
}
|
||||
|
@ -24,13 +24,13 @@ class AllocationTransformer extends BaseClientTransformer
|
||||
*/
|
||||
public function transform(Allocation $model)
|
||||
{
|
||||
$model->loadMissing('server');
|
||||
|
||||
return [
|
||||
'id' => $model->id,
|
||||
'ip' => $model->ip,
|
||||
'alias' => $model->ip_alias,
|
||||
'ip_alias' => $model->ip_alias,
|
||||
'port' => $model->port,
|
||||
'default' => $model->getRelation('server')->allocation_id === $model->id,
|
||||
'notes' => $model->notes,
|
||||
'is_default' => $model->server->allocation_id === $model->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,15 @@ namespace Pterodactyl\Transformers\Api\Client;
|
||||
use Pterodactyl\Models\Egg;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Subuser;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
|
||||
class ServerTransformer extends BaseClientTransformer
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $defaultIncludes = ['allocations'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@ -41,10 +47,6 @@ class ServerTransformer extends BaseClientTransformer
|
||||
'port' => $server->node->daemonSFTP,
|
||||
],
|
||||
'description' => $server->description,
|
||||
'allocation' => [
|
||||
'ip' => $server->allocation->alias,
|
||||
'port' => $server->allocation->port,
|
||||
],
|
||||
'limits' => [
|
||||
'memory' => $server->memory,
|
||||
'swap' => $server->swap,
|
||||
@ -62,6 +64,22 @@ class ServerTransformer extends BaseClientTransformer
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allocations associated with this server.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return \League\Fractal\Resource\Collection
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeAllocations(Server $server)
|
||||
{
|
||||
return $this->collection(
|
||||
$server->allocations,
|
||||
$this->makeTransformer(AllocationTransformer::class),
|
||||
Allocation::RESOURCE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the egg associated with this server.
|
||||
*
|
||||
|
23
babel.config.js
Normal file
23
babel.config.js
Normal file
@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@babel/typescript',
|
||||
['@babel/env', {
|
||||
modules: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3,
|
||||
}],
|
||||
'@babel/react',
|
||||
],
|
||||
plugins: [
|
||||
'babel-plugin-macros',
|
||||
'styled-components',
|
||||
'react-hot-loader/babel',
|
||||
'@babel/transform-runtime',
|
||||
'@babel/transform-react-jsx',
|
||||
'@babel/proposal-class-properties',
|
||||
'@babel/proposal-object-rest-spread',
|
||||
'@babel/proposal-optional-chaining',
|
||||
'@babel/proposal-nullish-coalescing-operator',
|
||||
'@babel/syntax-dynamic-import',
|
||||
],
|
||||
};
|
@ -37,6 +37,7 @@
|
||||
"psy/psysh": "^0.10.4",
|
||||
"s1lentium/iptools": "^1.1",
|
||||
"spatie/laravel-fractal": "^5.7",
|
||||
"spatie/laravel-query-builder": "^2.8",
|
||||
"staudenmeir/belongs-to-through": "^2.10",
|
||||
"symfony/yaml": "^4.4",
|
||||
"webmozart/assert": "^1.9"
|
||||
|
66
composer.lock
generated
66
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "155b8e930e604c0476fa975b1084ca3f",
|
||||
"content-hash": "d05ab995e4aff4b847ff2a027924065c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "appstract/laravel-blade-directives",
|
||||
@ -3361,6 +3361,70 @@
|
||||
],
|
||||
"time": "2020-03-02T18:40:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-query-builder",
|
||||
"version": "2.8.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-query-builder.git",
|
||||
"reference": "2737b2298e8bfeb632a80013646943307bf31775"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/2737b2298e8bfeb632a80013646943307bf31775",
|
||||
"reference": "2737b2298e8bfeb632a80013646943307bf31775",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/database": "~5.6.34|~5.7.0|~5.8.0|^6.0|^7.0",
|
||||
"illuminate/http": "~5.6.34|~5.7.0|~5.8.0|^6.0|^7.0",
|
||||
"illuminate/support": "~5.6.34|~5.7.0|~5.8.0|^6.0|^7.0",
|
||||
"php": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"orchestra/testbench": "~3.6.0|~3.7.0|~3.8.0|^4.0|^5.0",
|
||||
"phpunit/phpunit": "^7.0|^8.0|^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Spatie\\QueryBuilder\\QueryBuilderServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\QueryBuilder\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alex Vanderbist",
|
||||
"email": "alex@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Easily build Eloquent queries from API requests",
|
||||
"homepage": "https://github.com/spatie/laravel-query-builder",
|
||||
"keywords": [
|
||||
"laravel-query-builder",
|
||||
"spatie"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "custom"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-25T09:36:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "staudenmeir/belongs-to-through",
|
||||
"version": "v2.10",
|
||||
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddNotesColumnForAllocations extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('allocations', function (Blueprint $table) {
|
||||
$table->string('notes')->nullable()->after('server_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('allocations', function (Blueprint $table) {
|
||||
$table->dropColumn('notes');
|
||||
});
|
||||
}
|
||||
}
|
105
package.json
105
package.json
@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "pterodactyl-panel",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.19",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.19",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.9.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.4",
|
||||
"@types/react-google-recaptcha": "^1.1.1",
|
||||
"axios": "^0.19.0",
|
||||
"axios": "^0.19.2",
|
||||
"ayu-ace": "^2.0.4",
|
||||
"brace": "^0.11.1",
|
||||
"chart.js": "^2.8.0",
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^1.29.0",
|
||||
"easy-peasy": "^3.2.3",
|
||||
"date-fns": "^2.14.0",
|
||||
"debounce": "^1.2.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"easy-peasy": "^3.3.1",
|
||||
"events": "^3.0.0",
|
||||
"formik": "^2.1.4",
|
||||
"i18next": "^19.0.0",
|
||||
@ -19,26 +20,26 @@
|
||||
"i18next-localstorage-backend": "^3.0.0",
|
||||
"i18next-xhr-backend": "^3.2.2",
|
||||
"jquery": "^3.3.1",
|
||||
"lodash-es": "^4.17.15",
|
||||
"path": "^0.12.7",
|
||||
"query-string": "^6.7.0",
|
||||
"react": "^16.12.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "npm:@hot-loader/react-dom",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-google-recaptcha": "^2.0.1",
|
||||
"react-hot-loader": "^4.12.18",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-i18next": "^11.2.1",
|
||||
"react-redux": "^7.1.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"sockette": "^2.0.6",
|
||||
"styled-components": "^4.4.1",
|
||||
"styled-components": "^5.1.1",
|
||||
"styled-components-breakpoint": "^3.0.0-preview.20",
|
||||
"use-react-router": "^1.0.7",
|
||||
"swr": "^0.2.3",
|
||||
"uuid": "^3.3.2",
|
||||
"xterm": "^3.14.4",
|
||||
"xterm-addon-attach": "^0.1.0",
|
||||
"xterm-addon-fit": "^0.1.0",
|
||||
"yup": "^0.27.0"
|
||||
"yup": "^0.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.5",
|
||||
@ -47,75 +48,65 @@
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.10.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.5",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/runtime": "^7.7.5",
|
||||
"@types/chart.js": "^2.8.5",
|
||||
"@types/classnames": "^2.2.8",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/events": "^3.0.0",
|
||||
"@types/feather-icons": "^4.7.0",
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"@types/node": "^12.6.9",
|
||||
"@types/query-string": "^6.3.0",
|
||||
"@types/react": "^16.9.15",
|
||||
"@types/react-dom": "^16.9.4",
|
||||
"@types/react": "^16.9.41",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-redux": "^7.1.1",
|
||||
"@types/react-router": "^5.1.3",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"@types/react-transition-group": "^2.9.2",
|
||||
"@types/styled-components": "^4.4.0",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@types/uuid": "^3.4.5",
|
||||
"@types/webpack-env": "^1.13.6",
|
||||
"@types/yup": "^0.26.17",
|
||||
"@typescript-eslint/eslint-plugin": "^2.19.0",
|
||||
"@typescript-eslint/parser": "^2.19.0",
|
||||
"@types/webpack-env": "^1.15.2",
|
||||
"@types/yup": "^0.29.3",
|
||||
"@typescript-eslint/eslint-plugin": "^3.5.0",
|
||||
"@typescript-eslint/parser": "^3.5.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-styled-components": "^1.10.6",
|
||||
"babel-plugin-tailwind-components": "^0.5.10",
|
||||
"babel-plugin-styled-components": "^1.10.7",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^3.2.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.17.3",
|
||||
"eslint": "^7.4.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-node": "^9.1.0",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-react-hooks": "^2.1.2",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^1.5.0",
|
||||
"glob-all": "^3.1.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"postcss": "^7.0.24",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"precss": "^4.0.0",
|
||||
"purgecss-webpack-plugin": "^1.6.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.20.3",
|
||||
"eslint-plugin-react-hooks": "^4.0.5",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"fork-ts-checker-webpack-plugin": "^5.0.6",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"resolve-url-loader": "^3.0.0",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"style-loader": "^0.23.1",
|
||||
"tailwindcss": "^0.7.4",
|
||||
"terser-webpack-plugin": "^1.3.0",
|
||||
"ts-loader": "^6.2.1",
|
||||
"typescript": "^3.7.5",
|
||||
"webpack": "^4.41.2",
|
||||
"source-map-loader": "^1.0.1",
|
||||
"style-loader": "^1.2.1",
|
||||
"svg-url-loader": "^6.0.0",
|
||||
"tailwindcss": "^1.4.6",
|
||||
"terser-webpack-plugin": "^3.0.6",
|
||||
"twin.macro": "^1.4.1",
|
||||
"typescript": "^3.9.6",
|
||||
"typescript-plugin-tw-template": "^2.0.1",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-assets-manifest": "^3.1.1",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.9.0",
|
||||
"webpack-manifest-plugin": "^2.0.3",
|
||||
"webpack-bundle-analyzer": "^3.8.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"yarn-deduplicate": "^1.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf public/assets/*.{js,css,map}",
|
||||
"clean": "cd public/assets && find . \\( -name \"*.js\" -o -name \"*.map\" \\) -type f -delete",
|
||||
"lint": "eslint ./resources/scripts/**/*.{ts,tsx} --ext .ts,.tsx",
|
||||
"watch": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
|
||||
"build": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --progress",
|
||||
"build:production": "yarn run clean && cross-env NODE_ENV=production ./node_modules/.bin/webpack --mode production",
|
||||
"serve": "yarn run clean && cross-env PUBLIC_PATH=https://pterodactyl.test:8080 NODE_ENV=development webpack-dev-server --host 0.0.0.0 --hot --https --key /etc/ssl/private/pterodactyl.test-key.pem --cert /etc/ssl/private/pterodactyl.test.pem"
|
||||
"serve": "yarn run clean && cross-env PUBLIC_PATH=https://pterodactyl.test:8080 NODE_ENV=development TSC_WATCHFILE=UseFsEventsWithFallbackDynamicPolling webpack-dev-server --host 0.0.0.0 --hot --https --key /etc/ssl/private/pterodactyl.test-key.pem --cert /etc/ssl/private/pterodactyl.test.pem"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 0.5%",
|
||||
|
@ -1,38 +1,76 @@
|
||||
parser: "@typescript-eslint/parser"
|
||||
parserOptions:
|
||||
ecmaVersion: 6
|
||||
ecmaFeatures:
|
||||
jsx: true
|
||||
project: "./tsconfig.json"
|
||||
tsconfigRootDir: "./"
|
||||
settings:
|
||||
react:
|
||||
pragma: "React"
|
||||
version: "detect"
|
||||
linkComponents:
|
||||
- name: Link
|
||||
linkAttribute: to
|
||||
- name: NavLink
|
||||
linkAttribute: to
|
||||
env:
|
||||
browser: true
|
||||
es6: true
|
||||
plugins:
|
||||
- "@typescript-eslint"
|
||||
- "react"
|
||||
- "react-hooks"
|
||||
- "@typescript-eslint"
|
||||
extends:
|
||||
- "standard"
|
||||
- "plugin:react/recommended"
|
||||
- "plugin:@typescript-eslint/recommended"
|
||||
globals:
|
||||
tw: "readonly"
|
||||
rules:
|
||||
indent:
|
||||
- error
|
||||
- 4
|
||||
- SwitchCase: 1
|
||||
semi:
|
||||
- error
|
||||
- always
|
||||
comma-dangle:
|
||||
- error
|
||||
- always-multiline
|
||||
array-bracket-spacing:
|
||||
- warn
|
||||
- always
|
||||
"react-hooks/rules-of-hooks":
|
||||
- error
|
||||
"react-hooks/exhaustive-deps": 0
|
||||
"@typescript-eslint/explicit-function-return-type": 0
|
||||
"@typescript-eslint/explicit-member-accessibility": 0
|
||||
"@typescript-eslint/ban-ts-ignore": 0
|
||||
"@typescript-eslint/no-unused-vars": 0
|
||||
"@typescript-eslint/no-explicit-any": 0
|
||||
"@typescript-eslint/no-non-null-assertion": 0
|
||||
"@typescript-eslint/ban-ts-comment": 0
|
||||
# This would be nice to have, but don't want to deal with the warning spam at the moment.
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0
|
||||
no-restricted-imports:
|
||||
- error
|
||||
- paths:
|
||||
- name: styled-components
|
||||
message: Please import from styled-components/macro.
|
||||
patterns:
|
||||
- "!styled-components/macro"
|
||||
# Not sure, this rule just doesn't work right and is protected by our use of Typescript anyways
|
||||
# so I'm just not going to worry about it.
|
||||
"react/prop-types": 0
|
||||
"react/display-name": 0
|
||||
"react/jsx-indent-props":
|
||||
- warn
|
||||
- 4
|
||||
"react/jsx-boolean-value":
|
||||
- warn
|
||||
- never
|
||||
"react/jsx-closing-bracket-location":
|
||||
- 1
|
||||
- "line-aligned"
|
||||
"react/jsx-closing-tag-location": 1
|
||||
overrides:
|
||||
- files:
|
||||
- "**/*.tsx"
|
||||
|
@ -1,21 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
import { SwitchTransition } from 'react-transition-group';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
type Props = Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>;
|
||||
const StyledSwitchTransition = styled(SwitchTransition)`
|
||||
${tw`relative`};
|
||||
|
||||
& section {
|
||||
${tw`absolute w-full top-0 left-0`};
|
||||
}
|
||||
`;
|
||||
|
||||
export default ({ children }: Props) => (
|
||||
const TransitionRouter: React.FC = ({ children }) => (
|
||||
<Route
|
||||
render={({ location }) => (
|
||||
<TransitionGroup className={'route-transition-group'}>
|
||||
<CSSTransition key={location.key} timeout={250} in={true} appear={true} classNames={'fade'}>
|
||||
<StyledSwitchTransition>
|
||||
<Fade timeout={150} key={location.key} in appear unmountOnExit>
|
||||
<section>
|
||||
{children}
|
||||
</section>
|
||||
</CSSTransition>
|
||||
</TransitionGroup>
|
||||
</Fade>
|
||||
</StyledSwitchTransition>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
export default TransitionRouter;
|
||||
|
@ -3,13 +3,13 @@ import { ApiKey, rawDataToApiKey } from '@/api/account/getApiKeys';
|
||||
|
||||
export default (description: string, allowedIps: string): Promise<ApiKey & { secretToken: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(`/api/client/account/api-keys`, {
|
||||
http.post('/api/client/account/api-keys', {
|
||||
description,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
allowed_ips: allowedIps.length > 0 ? allowedIps.split('\n') : [],
|
||||
})
|
||||
.then(({ data }) => resolve({
|
||||
...rawDataToApiKey(data.attributes),
|
||||
// eslint-disable-next-line camelcase
|
||||
secretToken: data.meta?.secret_token ?? '',
|
||||
}))
|
||||
.catch(reject);
|
||||
|
@ -9,10 +9,8 @@ interface Data {
|
||||
export default ({ current, password, confirmPassword }: Data): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.put('/api/client/account/password', {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
current_password: current,
|
||||
password: password,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
password_confirmation: confirmPassword,
|
||||
})
|
||||
.then(() => resolve())
|
||||
|
@ -4,11 +4,9 @@ import { LoginResponse } from '@/api/auth/login';
|
||||
export default (token: string, code: string, recoveryToken?: string): Promise<LoginResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post('/auth/login/checkpoint', {
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
confirmation_token: token,
|
||||
authentication_code: code,
|
||||
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
|
||||
/* eslint-enable @typescript-eslint/camelcase */
|
||||
})
|
||||
.then(response => resolve({
|
||||
complete: response.data.data.complete,
|
||||
|
@ -17,7 +17,6 @@ export default (email: string, data: Data): Promise<PasswordResetResponse> => {
|
||||
email,
|
||||
token: data.token,
|
||||
password: data.password,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
password_confirmation: data.passwordConfirmation,
|
||||
})
|
||||
.then(response => resolve({
|
||||
|
@ -3,16 +3,15 @@ import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
||||
|
||||
export default (query?: string, includeAdmin?: boolean): Promise<PaginatedResult<Server>> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client`, {
|
||||
http.get('/api/client', {
|
||||
params: {
|
||||
include: [ 'allocation' ],
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
filter: includeAdmin ? 'all' : undefined,
|
||||
query,
|
||||
type: includeAdmin ? 'all' : undefined,
|
||||
'filter[name]': query,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => resolve({
|
||||
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum.attributes)),
|
||||
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum)),
|
||||
pagination: getPaginationSet(data.meta.pagination),
|
||||
}))
|
||||
.catch(reject);
|
||||
|
@ -3,7 +3,7 @@ import http from '@/api/http';
|
||||
|
||||
export default (): Promise<PanelPermissions> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/permissions`)
|
||||
http.get('/api/client/permissions')
|
||||
.then(({ data }) => resolve(data.attributes.permissions))
|
||||
.catch(reject);
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ const http: AxiosInstance = axios.create({
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': (window as any).X_CSRF_TOKEN as string || '',
|
||||
},
|
||||
@ -75,12 +75,15 @@ export interface FractalResponseData {
|
||||
object: string;
|
||||
attributes: {
|
||||
[k: string]: any;
|
||||
relationships?: {
|
||||
[k: string]: FractalResponseData;
|
||||
};
|
||||
relationships?: Record<string, FractalResponseData | FractalResponseList>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FractalResponseList {
|
||||
object: 'list';
|
||||
data: FractalResponseData[];
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
pagination: PaginationDataSet;
|
||||
|
@ -14,23 +14,21 @@ export interface FileObject {
|
||||
modifiedAt: Date;
|
||||
}
|
||||
|
||||
export default (uuid: string, directory?: string): Promise<FileObject[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}/files/list`, {
|
||||
params: { directory },
|
||||
})
|
||||
.then(response => resolve((response.data.data || []).map((item: any): FileObject => ({
|
||||
uuid: v4(),
|
||||
name: item.attributes.name,
|
||||
mode: item.attributes.mode,
|
||||
size: Number(item.attributes.size),
|
||||
isFile: item.attributes.is_file,
|
||||
isSymlink: item.attributes.is_symlink,
|
||||
isEditable: item.attributes.is_editable,
|
||||
mimetype: item.attributes.mimetype,
|
||||
createdAt: new Date(item.attributes.created_at),
|
||||
modifiedAt: new Date(item.attributes.modified_at),
|
||||
}))))
|
||||
.catch(reject);
|
||||
export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
|
||||
const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
|
||||
params: { directory },
|
||||
});
|
||||
|
||||
return (data.data || []).map((item: any): FileObject => ({
|
||||
uuid: v4(),
|
||||
name: item.attributes.name,
|
||||
mode: item.attributes.mode,
|
||||
size: Number(item.attributes.size),
|
||||
isFile: item.attributes.is_file,
|
||||
isSymlink: item.attributes.is_symlink,
|
||||
isEditable: item.attributes.is_editable,
|
||||
mimetype: item.attributes.mimetype,
|
||||
createdAt: new Date(item.attributes.created_at),
|
||||
modifiedAt: new Date(item.attributes.modified_at),
|
||||
}));
|
||||
};
|
||||
|
@ -8,9 +8,7 @@ interface Data {
|
||||
export default (uuid: string, { renameFrom, renameTo }: Data): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.put(`/api/client/servers/${uuid}/files/rename`, {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
rename_from: renameFrom,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
rename_to: renameTo,
|
||||
})
|
||||
.then(() => resolve())
|
||||
|
@ -1,10 +1,13 @@
|
||||
import http from '@/api/http';
|
||||
import http, { FractalResponseData, FractalResponseList } from '@/api/http';
|
||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||
|
||||
export interface Allocation {
|
||||
id: number;
|
||||
ip: string;
|
||||
alias: string | null;
|
||||
port: number;
|
||||
default: boolean;
|
||||
notes: string | null;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
@ -35,7 +38,7 @@ export interface Server {
|
||||
isInstalling: boolean;
|
||||
}
|
||||
|
||||
export const rawDataToServerObject = (data: any): Server => ({
|
||||
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
|
||||
id: data.identifier,
|
||||
uuid: data.uuid,
|
||||
name: data.name,
|
||||
@ -45,24 +48,20 @@ export const rawDataToServerObject = (data: any): Server => ({
|
||||
port: data.sftp_details.port,
|
||||
},
|
||||
description: data.description ? ((data.description.length > 0) ? data.description : null) : null,
|
||||
allocations: [ {
|
||||
ip: data.allocation.ip,
|
||||
alias: null,
|
||||
port: data.allocation.port,
|
||||
default: true,
|
||||
} ],
|
||||
limits: { ...data.limits },
|
||||
featureLimits: { ...data.feature_limits },
|
||||
isSuspended: data.is_suspended,
|
||||
isInstalling: data.is_installing,
|
||||
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
||||
});
|
||||
|
||||
export default (uuid: string): Promise<[ Server, string[] ]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}`)
|
||||
.then(({ data }) => resolve([
|
||||
rawDataToServerObject(data.attributes),
|
||||
data.meta?.is_server_owner ? ['*'] : (data.meta?.user_permissions || []),
|
||||
rawDataToServerObject(data),
|
||||
// eslint-disable-next-line camelcase
|
||||
data.meta?.is_server_owner ? [ '*' ] : (data.meta?.user_permissions || []),
|
||||
]))
|
||||
.catch(reject);
|
||||
});
|
||||
|
@ -18,7 +18,7 @@ export const rawDataToServerDatabase = (data: any): ServerDatabase => ({
|
||||
password: data.relationships && data.relationships.password ? data.relationships.password.attributes.password : undefined,
|
||||
});
|
||||
|
||||
export default (uuid: string, includePassword: boolean = true): Promise<ServerDatabase[]> => {
|
||||
export default (uuid: string, includePassword = true): Promise<ServerDatabase[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}/databases`, {
|
||||
params: includePassword ? { include: 'password' } : undefined,
|
||||
|
@ -0,0 +1,4 @@
|
||||
import { Allocation } from '@/api/server/getServer';
|
||||
import http from '@/api/http';
|
||||
|
||||
export default async (uuid: string, id: number): Promise<Allocation> => await http.delete(`/api/client/servers/${uuid}/network/allocations/${id}`);
|
@ -0,0 +1,9 @@
|
||||
import http from '@/api/http';
|
||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||
import { Allocation } from '@/api/server/getServer';
|
||||
|
||||
export default async (uuid: string): Promise<Allocation[]> => {
|
||||
const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`);
|
||||
|
||||
return (data.data || []).map(rawDataToServerAllocation);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { Allocation } from '@/api/server/getServer';
|
||||
import http from '@/api/http';
|
||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||
|
||||
export default async (uuid: string, id: number): Promise<Allocation> => {
|
||||
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}/primary`);
|
||||
|
||||
return rawDataToServerAllocation(data);
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { Allocation } from '@/api/server/getServer';
|
||||
import http from '@/api/http';
|
||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||
|
||||
export default async (uuid: string, id: number, notes: string | null): Promise<Allocation> => {
|
||||
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}`, { notes });
|
||||
|
||||
return rawDataToServerAllocation(data);
|
||||
};
|
@ -6,4 +6,4 @@ export default (uuid: string): Promise<void> => {
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -11,7 +11,6 @@ export default (uuid: string, schedule: number, task: number | undefined, { time
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`, {
|
||||
...data,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
time_offset: timeOffset,
|
||||
})
|
||||
.then(({ data }) => resolve(rawDataToServerTask(data.attributes)))
|
||||
|
@ -5,5 +5,5 @@ export default (uuid: string, scheduleId: number, taskId: number): Promise<void>
|
||||
http.delete(`/api/client/servers/${uuid}/schedules/${scheduleId}/tasks/${taskId}`)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
})
|
||||
});
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ export default (uuid: string, schedule: number): Promise<Schedule> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, {
|
||||
params: {
|
||||
include: ['tasks'],
|
||||
include: [ 'tasks' ],
|
||||
},
|
||||
})
|
||||
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
|
||||
|
@ -64,7 +64,7 @@ export default (uuid: string): Promise<Schedule[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}/schedules`, {
|
||||
params: {
|
||||
include: ['tasks'],
|
||||
include: [ 'tasks' ],
|
||||
},
|
||||
})
|
||||
.then(({ data }) => resolve((data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes))))
|
||||
|
@ -15,4 +15,4 @@ export default (uuid: string, params: Params, subuser?: Subuser): Promise<Subuse
|
||||
.then(data => resolve(rawDataToServerSubuser(data.data)))
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
11
resources/scripts/api/transformers.ts
Normal file
11
resources/scripts/api/transformers.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Allocation } from '@/api/server/getServer';
|
||||
import { FractalResponseData } from '@/api/http';
|
||||
|
||||
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
|
||||
id: data.attributes.id,
|
||||
ip: data.attributes.ip,
|
||||
alias: data.attributes.ip_alias,
|
||||
port: data.attributes.port,
|
||||
notes: data.attributes.notes,
|
||||
isDefault: data.attributes.is_default,
|
||||
});
|
35
resources/scripts/assets/css/GlobalStylesheet.ts
Normal file
35
resources/scripts/assets/css/GlobalStylesheet.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import tw from 'twin.macro';
|
||||
import { createGlobalStyle } from 'styled-components/macro';
|
||||
|
||||
export default createGlobalStyle`
|
||||
body {
|
||||
${tw`font-sans bg-neutral-800 text-neutral-200`};
|
||||
letter-spacing: 0.015em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
${tw`font-medium tracking-normal font-header`};
|
||||
}
|
||||
|
||||
p {
|
||||
${tw`text-neutral-200 leading-snug font-sans`};
|
||||
}
|
||||
|
||||
form {
|
||||
${tw`m-0`};
|
||||
}
|
||||
|
||||
textarea, select, input, button, button:focus, button:focus-visible {
|
||||
${tw`outline-none`};
|
||||
}
|
||||
|
||||
input[type=number]::-webkit-outer-spin-button,
|
||||
input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield !important;
|
||||
}
|
||||
`;
|
@ -8,9 +8,10 @@ import ServerRouter from '@/routers/ServerRouter';
|
||||
import AuthenticationRouter from '@/routers/AuthenticationRouter';
|
||||
import { Provider } from 'react-redux';
|
||||
import { SiteSettings } from '@/state/settings';
|
||||
import { DefaultTheme, ThemeProvider } from 'styled-components';
|
||||
import ProgressBar from '@/components/elements/ProgressBar';
|
||||
import NotFound from '@/components/screens/NotFound';
|
||||
import tw from 'twin.macro';
|
||||
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
|
||||
|
||||
interface ExtendedWindow extends Window {
|
||||
SiteConfiguration?: SiteSettings;
|
||||
@ -18,24 +19,16 @@ interface ExtendedWindow extends Window {
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
/* eslint-disable camelcase */
|
||||
root_admin: boolean;
|
||||
use_totp: boolean;
|
||||
language: string;
|
||||
updated_at: string;
|
||||
created_at: string;
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
}
|
||||
|
||||
const theme: DefaultTheme = {
|
||||
breakpoints: {
|
||||
xs: 0,
|
||||
sm: 576,
|
||||
md: 768,
|
||||
lg: 992,
|
||||
xl: 1200,
|
||||
},
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
|
||||
if (PterodactylUser && !store.getState().user.data) {
|
||||
@ -56,11 +49,12 @@ const App = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<>
|
||||
<GlobalStylesheet/>
|
||||
<StoreProvider store={store}>
|
||||
<Provider store={store}>
|
||||
<ProgressBar/>
|
||||
<div className={'mx-auto w-auto'}>
|
||||
<div css={tw`mx-auto w-auto`}>
|
||||
<BrowserRouter basename={'/'} key={'root-router'}>
|
||||
<Switch>
|
||||
<Route path="/server/:id" component={ServerRouter}/>
|
||||
@ -72,7 +66,7 @@ const App = () => {
|
||||
</div>
|
||||
</Provider>
|
||||
</StoreProvider>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,38 +1,35 @@
|
||||
import React from 'react';
|
||||
import MessageBox from '@/components/MessageBox';
|
||||
import { State, useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
type Props = Readonly<{
|
||||
byKey?: string;
|
||||
spacerClass?: string;
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export default ({ className, spacerClass, byKey }: Props) => {
|
||||
const flashes = useStoreState((state: State<ApplicationStore>) => state.flashes.items);
|
||||
|
||||
let filtered = flashes;
|
||||
if (byKey) {
|
||||
filtered = flashes.filter(flash => flash.key === byKey);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const FlashMessageRender = ({ byKey, className }: Props) => {
|
||||
const flashes = useStoreState(state => state.flashes.items.filter(
|
||||
flash => byKey ? flash.key === byKey : true,
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{
|
||||
filtered.map((flash, index) => (
|
||||
<React.Fragment key={flash.id || flash.type + flash.message}>
|
||||
{index > 0 && <div className={spacerClass || 'mt-2'}></div>}
|
||||
<MessageBox type={flash.type} title={flash.title}>
|
||||
{flash.message}
|
||||
</MessageBox>
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
flashes.length ?
|
||||
<div className={className}>
|
||||
{
|
||||
flashes.map((flash, index) => (
|
||||
<React.Fragment key={flash.id || flash.type + flash.message}>
|
||||
{index > 0 && <div css={tw`mt-2`}></div>}
|
||||
<MessageBox type={flash.type} title={flash.title}>
|
||||
{flash.message}
|
||||
</MessageBox>
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
export default FlashMessageRender;
|
||||
|
@ -1,4 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import tw, { TwStyle } from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
|
||||
|
||||
@ -8,11 +10,60 @@ interface Props {
|
||||
type?: FlashMessageType;
|
||||
}
|
||||
|
||||
export default ({ title, children, type }: Props) => (
|
||||
<div className={`lg:inline-flex alert ${type}`} role={'alert'}>
|
||||
{title && <span className={'title'}>{title}</span>}
|
||||
<span className={'message'}>
|
||||
const styling = (type?: FlashMessageType): TwStyle | string => {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return tw`bg-red-600 border-red-800`;
|
||||
case 'info':
|
||||
return tw`bg-primary-600 border-primary-800`;
|
||||
case 'success':
|
||||
return tw`bg-green-600 border-green-800`;
|
||||
case 'warning':
|
||||
return tw`bg-yellow-600 border-yellow-800`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getBackground = (type?: FlashMessageType): TwStyle | string => {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return tw`bg-red-500`;
|
||||
case 'info':
|
||||
return tw`bg-primary-500`;
|
||||
case 'success':
|
||||
return tw`bg-green-500`;
|
||||
case 'warning':
|
||||
return tw`bg-yellow-500`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const Container = styled.div<{ $type?: FlashMessageType }>`
|
||||
${tw`p-2 border items-center leading-normal rounded flex w-full text-sm text-white`};
|
||||
${props => styling(props.$type)};
|
||||
`;
|
||||
Container.displayName = 'MessageBox.Container';
|
||||
|
||||
const MessageBox = ({ title, children, type }: Props) => (
|
||||
<Container css={tw`lg:inline-flex`} $type={type} role={'alert'}>
|
||||
{title &&
|
||||
<span
|
||||
className={'title'}
|
||||
css={[
|
||||
tw`flex rounded-full uppercase px-2 py-1 text-xs font-bold mr-3 leading-none`,
|
||||
getBackground(type),
|
||||
]}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
}
|
||||
<span css={tw`mr-2 text-left flex-auto`}>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
MessageBox.displayName = 'MessageBox';
|
||||
|
||||
export default MessageBox;
|
||||
|
@ -1,51 +1,77 @@
|
||||
import * as React from 'react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLayerGroup } from '@fortawesome/free-solid-svg-icons/faLayerGroup';
|
||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons/faUserCircle';
|
||||
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt';
|
||||
import { faSwatchbook } from '@fortawesome/free-solid-svg-icons/faSwatchbook';
|
||||
import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs';
|
||||
import { faCogs, faLayerGroup, faSignOutAlt, faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
|
||||
import SearchContainer from '@/components/dashboard/search/SearchContainer';
|
||||
import tw from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
// @ts-ignore
|
||||
import * as config from '@/../../tailwind.config.js';
|
||||
|
||||
const Navigation = styled.div`
|
||||
${tw`w-full bg-neutral-900 shadow-md`};
|
||||
|
||||
& > div {
|
||||
${tw`mx-auto w-full flex items-center`};
|
||||
}
|
||||
|
||||
& #logo {
|
||||
${tw`flex-1`};
|
||||
|
||||
& > a {
|
||||
${tw`text-2xl font-header px-4 no-underline text-neutral-200 hover:text-neutral-100 transition-colors duration-150`};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const RightNavigation = styled.div`
|
||||
${tw`flex h-full items-center justify-center`};
|
||||
|
||||
& > a, & > .navigation-link {
|
||||
${tw`flex items-center h-full no-underline text-neutral-300 px-6 cursor-pointer transition-all duration-150`};
|
||||
|
||||
&:active, &:hover {
|
||||
${tw`text-neutral-100 bg-black`};
|
||||
}
|
||||
|
||||
&:active, &:hover, &.active {
|
||||
box-shadow: inset 0 -2px ${config.theme.colors.cyan['700']};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default () => {
|
||||
const user = useStoreState((state: ApplicationStore) => state.user.data!);
|
||||
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||
|
||||
return (
|
||||
<div id={'navigation'}>
|
||||
<div className={'mx-auto w-full flex items-center'} style={{ maxWidth: '1200px', height: '3.5rem' }}>
|
||||
<Navigation>
|
||||
<div css={tw`mx-auto w-full flex items-center`} style={{ maxWidth: '1200px', height: '3.5rem' }}>
|
||||
<div id={'logo'}>
|
||||
<Link to={'/'}>
|
||||
{name}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={'right-navigation'}>
|
||||
<RightNavigation>
|
||||
<SearchContainer/>
|
||||
<NavLink to={'/'} exact={true}>
|
||||
<NavLink to={'/'} exact>
|
||||
<FontAwesomeIcon icon={faLayerGroup}/>
|
||||
</NavLink>
|
||||
<NavLink to={'/account'}>
|
||||
<FontAwesomeIcon icon={faUserCircle}/>
|
||||
</NavLink>
|
||||
{user.rootAdmin &&
|
||||
<a href={'/admin'} target={'_blank'}>
|
||||
<a href={'/admin'} target={'_blank'} rel={'noreferrer'}>
|
||||
<FontAwesomeIcon icon={faCogs}/>
|
||||
</a>
|
||||
}
|
||||
{process.env.NODE_ENV !== 'production' &&
|
||||
<NavLink to={'/design'}>
|
||||
<FontAwesomeIcon icon={faSwatchbook}/>
|
||||
</NavLink>
|
||||
}
|
||||
<a href={'/auth/logout'}>
|
||||
<FontAwesomeIcon icon={faSignOutAlt}/>
|
||||
</a>
|
||||
</div>
|
||||
</RightNavigation>
|
||||
</div>
|
||||
</div>
|
||||
</Navigation>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import MessageBox from '@/components/MessageBox';
|
||||
|
||||
export default ({ message }: { message: string | undefined | null }) => (
|
||||
!message ?
|
||||
null
|
||||
:
|
||||
<div className={'mb-4'}>
|
||||
<MessageBox type={'error'} title={'Error'}>
|
||||
{message}
|
||||
</MessageBox>
|
||||
</div>
|
||||
);
|
@ -1,13 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
export default class ServerOverviewContainer extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<div className={'mt-10'}>
|
||||
<NavLink className={'text-neutral-100 text-sm block mb-2 no-underline hover:underline'} to={'/account'}>Account</NavLink>
|
||||
<NavLink className={'text-neutral-100 text-sm block mb-2 no-underline hover:underline'} to={'/account/design'}>Design</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import { ApplicationStore } from '@/state';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
interface Values {
|
||||
email: string;
|
||||
@ -43,33 +45,30 @@ export default () => {
|
||||
{({ isSubmitting }) => (
|
||||
<LoginFormContainer
|
||||
title={'Request Password Reset'}
|
||||
className={'w-full flex'}
|
||||
css={tw`w-full flex`}
|
||||
>
|
||||
<Field
|
||||
light={true}
|
||||
light
|
||||
label={'Email'}
|
||||
description={'Enter your account email address to receive instructions on resetting your password.'}
|
||||
name={'email'}
|
||||
type={'email'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
<div css={tw`mt-6`}>
|
||||
<Button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo flex justify-center'}
|
||||
size={'xlarge'}
|
||||
disabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<div className={'spinner-circle spinner-sm spinner-white'}></div>
|
||||
:
|
||||
'Send Email'
|
||||
}
|
||||
</button>
|
||||
Send Email
|
||||
</Button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
type={'button'}
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
|
@ -5,19 +5,19 @@ import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { ActionCreator } from 'easy-peasy';
|
||||
import { StaticContext } from 'react-router';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { useFormikContext, withFormik } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { FlashStore } from '@/state/flashes';
|
||||
import Field from '@/components/elements/Field';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
interface Values {
|
||||
code: string;
|
||||
recoveryCode: '',
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
|
||||
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }>
|
||||
|
||||
type Props = OwnProps & {
|
||||
addError: ActionCreator<FlashStore['addError']['payload']>;
|
||||
@ -29,13 +29,10 @@ const LoginCheckpointContainer = () => {
|
||||
const [ isMissingDevice, setIsMissingDevice ] = useState(false);
|
||||
|
||||
return (
|
||||
<LoginFormContainer
|
||||
title={'Device Checkpoint'}
|
||||
className={'w-full flex'}
|
||||
>
|
||||
<div className={'mt-6'}>
|
||||
<LoginFormContainer title={'Device Checkpoint'} css={tw`w-full flex`}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
light={true}
|
||||
light
|
||||
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
||||
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
|
||||
description={
|
||||
@ -44,38 +41,35 @@ const LoginCheckpointContainer = () => {
|
||||
: 'Enter the two-factor token generated by your device.'
|
||||
}
|
||||
type={isMissingDevice ? 'text' : 'number'}
|
||||
autoFocus={true}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
<div css={tw`mt-6`}>
|
||||
<Button
|
||||
size={'xlarge'}
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
disabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
||||
:
|
||||
'Continue'
|
||||
}
|
||||
</button>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<span
|
||||
onClick={() => {
|
||||
setFieldValue('code', '');
|
||||
setFieldValue('recoveryCode', '');
|
||||
setIsMissingDevice(s => !s);
|
||||
}}
|
||||
className={'cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
||||
css={tw`cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
|
@ -10,7 +10,8 @@ import Field from '@/components/elements/Field';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { FlashMessage } from '@/state/flashes';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
type OwnProps = RouteComponentProps & {
|
||||
clearFlashes: ActionCreator<void>;
|
||||
@ -34,38 +35,27 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ref.current && ref.current.render()}
|
||||
<LoginFormContainer
|
||||
title={'Login to Continue'}
|
||||
className={'w-full flex'}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<label htmlFor={'username'}>Username or Email</label>
|
||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}>
|
||||
<Field
|
||||
type={'text'}
|
||||
label={'Username or Email'}
|
||||
id={'username'}
|
||||
name={'username'}
|
||||
className={'input'}
|
||||
light
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'password'}>Password</label>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
type={'password'}
|
||||
label={'Password'}
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
className={'input'}
|
||||
light
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
||||
:
|
||||
'Login'
|
||||
}
|
||||
</button>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled &&
|
||||
<ReCAPTCHA
|
||||
@ -80,10 +70,10 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||
onExpired={() => setFieldValue('recaptchaData', null)}
|
||||
/>
|
||||
}
|
||||
<div className={'mt-6 text-center'}>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/password'}
|
||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
@ -96,7 +86,7 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||
const EnhancedForm = withFormik<OwnProps, LoginData>({
|
||||
displayName: 'LoginContainerForm',
|
||||
|
||||
mapPropsToValues: (props) => ({
|
||||
mapPropsToValues: () => ({
|
||||
username: '',
|
||||
password: '',
|
||||
recaptchaData: null,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { Form } from 'formik';
|
||||
import styled from 'styled-components';
|
||||
import { breakpoint } from 'styled-components-breakpoint';
|
||||
import styled from 'styled-components/macro';
|
||||
import { breakpoint } from '@/theme';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
||||
title?: string;
|
||||
@ -29,27 +30,29 @@ const Container = styled.div`
|
||||
|
||||
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
||||
<Container>
|
||||
{title && <h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
{title &&
|
||||
<h2 css={tw`text-3xl text-center text-neutral-100 font-medium py-4`}>
|
||||
{title}
|
||||
</h2>}
|
||||
<FlashMessageRender className={'mb-2 px-1'}/>
|
||||
</h2>
|
||||
}
|
||||
<FlashMessageRender css={tw`mb-2 px-1`}/>
|
||||
<Form {...props} ref={ref}>
|
||||
<div className={'md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1'}>
|
||||
<div className={'flex-none select-none mb-6 md:mb-0 self-center'}>
|
||||
<img src={'/assets/svgs/pterodactyl.svg'} className={'block w-48 md:w-64 mx-auto'}/>
|
||||
<div css={tw`md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1`}>
|
||||
<div css={tw`flex-none select-none mb-6 md:mb-0 self-center`}>
|
||||
<img src={'/assets/svgs/pterodactyl.svg'} css={tw`block w-48 md:w-64 mx-auto`}/>
|
||||
</div>
|
||||
<div className={'flex-1'}>
|
||||
<div css={tw`flex-1`}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
<p className={'text-center text-neutral-500 text-xs mt-4'}>
|
||||
<p css={tw`text-center text-neutral-500 text-xs mt-4`}>
|
||||
© 2015 - 2020
|
||||
<a
|
||||
rel={'noopener nofollow'}
|
||||
rel={'noopener nofollow noreferrer'}
|
||||
href={'https://pterodactyl.io'}
|
||||
target={'_blank'}
|
||||
className={'no-underline text-neutral-500 hover:text-neutral-300'}
|
||||
css={tw`no-underline text-neutral-500 hover:text-neutral-300`}
|
||||
>
|
||||
Pterodactyl Software
|
||||
</a>
|
||||
|
@ -7,19 +7,19 @@ import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, ref, string } from 'yup';
|
||||
import Field from '@/components/elements/Field';
|
||||
|
||||
type Props = Readonly<RouteComponentProps<{ token: string }> & {}>;
|
||||
import Input from '@/components/elements/Input';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
interface Values {
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
}
|
||||
|
||||
export default ({ match, history, location }: Props) => {
|
||||
export default ({ match, location }: RouteComponentProps<{ token: string }>) => {
|
||||
const [ email, setEmail ] = useState('');
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
@ -56,52 +56,50 @@ export default ({ match, history, location }: Props) => {
|
||||
.min(8, 'Your new password should be at least 8 characters in length.'),
|
||||
passwordConfirmation: string()
|
||||
.required('Your new password does not match.')
|
||||
.oneOf([ref('password'), null], 'Your new password does not match.'),
|
||||
// @ts-ignore
|
||||
.oneOf([ ref('password'), null ], 'Your new password does not match.'),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<LoginFormContainer
|
||||
title={'Reset Password'}
|
||||
className={'w-full flex'}
|
||||
css={tw`w-full flex`}
|
||||
>
|
||||
<div>
|
||||
<label>Email</label>
|
||||
<input className={'input'} value={email} disabled={true}/>
|
||||
<Input value={email} isLight disabled/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
light={true}
|
||||
light
|
||||
label={'New Password'}
|
||||
name={'password'}
|
||||
type={'password'}
|
||||
description={'Passwords must be at least 8 characters in length.'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
light={true}
|
||||
light
|
||||
label={'Confirm New Password'}
|
||||
name={'passwordConfirmation'}
|
||||
type={'password'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
<div css={tw`mt-6`}>
|
||||
<Button
|
||||
size={'xlarge'}
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
disabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
||||
:
|
||||
'Reset Password'
|
||||
}
|
||||
</button>
|
||||
Reset Password
|
||||
</Button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
|
@ -4,16 +4,17 @@ import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
|
||||
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faKey } from '@fortawesome/free-solid-svg-icons/faKey';
|
||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
||||
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
||||
import deleteApiKey from '@/api/account/deleteApiKey';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import format from 'date-fns/format';
|
||||
import { format } from 'date-fns';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import tw from 'twin.macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
|
||||
export default () => {
|
||||
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
|
||||
@ -48,18 +49,18 @@ export default () => {
|
||||
|
||||
return (
|
||||
<PageContentBlock>
|
||||
<FlashMessageRender byKey={'account'} className={'mb-4'}/>
|
||||
<div className={'flex'}>
|
||||
<ContentBox title={'Create API Key'} className={'flex-1'}>
|
||||
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
|
||||
<div css={tw`flex`}>
|
||||
<ContentBox title={'Create API Key'} css={tw`flex-1`}>
|
||||
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
|
||||
</ContentBox>
|
||||
<ContentBox title={'API Keys'} className={'ml-10 flex-1'}>
|
||||
<ContentBox title={'API Keys'} css={tw`ml-10 flex-1`}>
|
||||
<SpinnerOverlay visible={loading}/>
|
||||
{deleteIdentifier &&
|
||||
<ConfirmationModal
|
||||
visible
|
||||
title={'Confirm key deletion'}
|
||||
buttonText={'Yes, delete key'}
|
||||
visible={true}
|
||||
onConfirmed={() => {
|
||||
doDeletion(deleteIdentifier);
|
||||
setDeleteIdentifier('');
|
||||
@ -72,38 +73,38 @@ export default () => {
|
||||
}
|
||||
{
|
||||
keys.length === 0 ?
|
||||
<p className={'text-center text-sm'}>
|
||||
<p css={tw`text-center text-sm`}>
|
||||
{loading ? 'Loading...' : 'No API keys exist for this account.'}
|
||||
</p>
|
||||
:
|
||||
keys.map(key => (
|
||||
<div
|
||||
keys.map((key, index) => (
|
||||
<GreyRowBox
|
||||
key={key.identifier}
|
||||
className={'grey-row-box bg-neutral-600 mb-2 flex items-center'}
|
||||
css={[ tw`bg-neutral-600 flex items-center`, index > 0 && tw`mt-2` ]}
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} className={'text-neutral-300'}/>
|
||||
<div className={'ml-4 flex-1'}>
|
||||
<p className={'text-sm'}>{key.description}</p>
|
||||
<p className={'text-2xs text-neutral-300 uppercase'}>
|
||||
Last
|
||||
used: {key.lastUsedAt ? format(key.lastUsedAt, 'MMM Do, YYYY HH:mm') : 'Never'}
|
||||
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`}/>
|
||||
<div css={tw`ml-4 flex-1`}>
|
||||
<p css={tw`text-sm`}>{key.description}</p>
|
||||
<p css={tw`text-2xs text-neutral-300 uppercase`}>
|
||||
Last used:
|
||||
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-sm ml-4'}>
|
||||
<code className={'font-mono py-1 px-2 bg-neutral-900 rounded'}>
|
||||
<p css={tw`text-sm ml-4`}>
|
||||
<code css={tw`font-mono py-1 px-2 bg-neutral-900 rounded`}>
|
||||
{key.identifier}
|
||||
</code>
|
||||
</p>
|
||||
<button
|
||||
className={'ml-4 p-2 text-sm'}
|
||||
css={tw`ml-4 p-2 text-sm`}
|
||||
onClick={() => setDeleteIdentifier(key.identifier)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTrashAlt}
|
||||
className={'text-neutral-400 hover:text-red-400 transition-colors duration-150'}
|
||||
css={tw`text-neutral-400 hover:text-red-400 transition-colors duration-150`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</GreyRowBox>
|
||||
))
|
||||
}
|
||||
</ContentBox>
|
||||
|
@ -3,9 +3,10 @@ import ContentBox from '@/components/elements/ContentBox';
|
||||
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
||||
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
||||
import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFactorForm';
|
||||
import styled from 'styled-components';
|
||||
import { breakpoint } from 'styled-components-breakpoint';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import tw from 'twin.macro';
|
||||
import { breakpoint } from '@/theme';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
${tw`flex flex-wrap my-10`};
|
||||
@ -31,13 +32,13 @@ export default () => {
|
||||
<UpdatePasswordForm/>
|
||||
</ContentBox>
|
||||
<ContentBox
|
||||
className={'mt-8 md:mt-0 md:ml-8'}
|
||||
css={tw`mt-8 md:mt-0 md:ml-8`}
|
||||
title={'Update Email Address'}
|
||||
showFlashes={'account:email'}
|
||||
>
|
||||
<UpdateEmailAddressForm/>
|
||||
</ContentBox>
|
||||
<ContentBox className={'xl:ml-8 mt-8 xl:mt-0'} title={'Configure Two Factor'}>
|
||||
<ContentBox css={tw`xl:ml-8 mt-8 xl:mt-0`} title={'Configure Two Factor'}>
|
||||
<ConfigureTwoFactorForm/>
|
||||
</ContentBox>
|
||||
</Container>
|
||||
|
@ -10,6 +10,7 @@ import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { usePersistedState } from '@/plugins/usePersistedState';
|
||||
import Switch from '@/components/elements/Switch';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
export default () => {
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
@ -37,10 +38,10 @@ export default () => {
|
||||
|
||||
return (
|
||||
<PageContentBlock>
|
||||
<FlashMessageRender className={'mb-4'}/>
|
||||
<FlashMessageRender css={tw`mb-4`}/>
|
||||
{rootAdmin &&
|
||||
<div className={'mb-2 flex justify-end items-center'}>
|
||||
<p className={'uppercase text-xs text-neutral-400 mr-2'}>
|
||||
<div css={tw`mb-2 flex justify-end items-center`}>
|
||||
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
|
||||
{showAdmin ? 'Showing all servers' : 'Showing your servers'}
|
||||
</p>
|
||||
<Switch
|
||||
@ -51,14 +52,16 @@ export default () => {
|
||||
</div>
|
||||
}
|
||||
{loading ?
|
||||
<Spinner centered={true} size={'large'}/>
|
||||
<Spinner centered size={'large'}/>
|
||||
:
|
||||
servers.length > 0 ?
|
||||
servers.map(server => (
|
||||
<ServerRow key={server.uuid} server={server} className={'mt-2'}/>
|
||||
servers.map((server, index) => (
|
||||
<div key={server.uuid} css={index > 0 ? tw`mt-2` : undefined}>
|
||||
<ServerRow server={server}/>
|
||||
</div>
|
||||
))
|
||||
:
|
||||
<p className={'text-center text-sm text-neutral-400'}>
|
||||
<p css={tw`text-center text-sm text-neutral-400`}>
|
||||
There are no servers associated with your account.
|
||||
</p>
|
||||
}
|
||||
|
@ -1,82 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
|
||||
export default class DesignElementsContainer extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={'my-10'}>
|
||||
<div className={'flex'}>
|
||||
<ContentBox
|
||||
className={'flex-1 mr-4'}
|
||||
title={'A Special Announcement'}
|
||||
borderColor={'border-primary-400'}
|
||||
>
|
||||
<p className={'text-neutral-200 text-sm'}>
|
||||
Your demands have been received: Dark Mode will be default in Pterodactyl 0.8!
|
||||
</p>
|
||||
<p><Link to={'/'}>Back</Link></p>
|
||||
</ContentBox>
|
||||
<div className={'ml-4 flex-1'}>
|
||||
<h2 className={'text-neutral-300 mb-2 px-4'}>Form Elements</h2>
|
||||
<div className={'bg-neutral-700 p-4 rounded shadow-lg border-t-4 border-primary-400'}>
|
||||
<label className={'uppercase text-neutral-200'}>Email</label>
|
||||
<input type={'text'} className={'input-dark'}/>
|
||||
<p className={'input-help'}>
|
||||
This is some descriptive helper text to explain how things work.
|
||||
</p>
|
||||
<div className={'mt-6'}/>
|
||||
<label className={'uppercase text-neutral-200'}>Username</label>
|
||||
<input type={'text'} className={'input-dark error'}/>
|
||||
<p className={'input-help'}>
|
||||
This field has an error.
|
||||
</p>
|
||||
<div className={'mt-6'}/>
|
||||
<label className={'uppercase text-neutral-200'}>Disabled Field</label>
|
||||
<input type={'text'} className={'input-dark'} disabled={true}/>
|
||||
<div className={'mt-6'}/>
|
||||
<label className={'uppercase text-neutral-200'}>Select</label>
|
||||
<select className={'input-dark'}>
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
<option>Option 3</option>
|
||||
</select>
|
||||
<div className={'mt-6'}/>
|
||||
<label className={'uppercase text-neutral-200'}>Textarea</label>
|
||||
<textarea className={'input-dark h-32'}></textarea>
|
||||
<div className={'mt-6'}/>
|
||||
<button className={'btn btn-primary btn-sm'}>
|
||||
Blue
|
||||
</button>
|
||||
<button className={'btn btn-grey btn-sm ml-2'}>
|
||||
Grey
|
||||
</button>
|
||||
<button className={'btn btn-green btn-sm ml-2'}>
|
||||
Green
|
||||
</button>
|
||||
<button className={'btn btn-red btn-sm ml-2'}>
|
||||
Red
|
||||
</button>
|
||||
<div className={'mt-6'}/>
|
||||
<button className={'btn btn-secondary btn-sm'}>
|
||||
Secondary
|
||||
</button>
|
||||
<button className={'btn btn-secondary btn-red btn-sm ml-2'}>
|
||||
Secondary Danger
|
||||
</button>
|
||||
<div className={'mt-6'}/>
|
||||
<button className={'btn btn-primary btn-lg'}>
|
||||
Large
|
||||
</button>
|
||||
<button className={'btn btn-primary btn-xs ml-2'}>
|
||||
Tiny
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,16 +1,13 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
|
||||
import { faEthernet } from '@fortawesome/free-solid-svg-icons/faEthernet';
|
||||
import { faMicrochip } from '@fortawesome/free-solid-svg-icons/faMicrochip';
|
||||
import { faMemory } from '@fortawesome/free-solid-svg-icons/faMemory';
|
||||
import { faHdd } from '@fortawesome/free-solid-svg-icons/faHdd';
|
||||
import { faServer, faEthernet, faMicrochip, faMemory, faHdd } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Server } from '@/api/server/getServer';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
|
||||
import { bytesToHuman } from '@/helpers';
|
||||
import classNames from 'classnames';
|
||||
import tw from 'twin.macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
|
||||
// Determines if the current value is in an alarm threshold so we can show it in red rather
|
||||
// than the more faded default style.
|
||||
@ -20,7 +17,7 @@ const isAlarmState = (current: number, limit: number): boolean => {
|
||||
return current / limitInBytes >= 0.90;
|
||||
};
|
||||
|
||||
export default ({ server, className }: { server: Server; className: string | undefined }) => {
|
||||
export default ({ server }: { server: Server }) => {
|
||||
const interval = useRef<number>(null);
|
||||
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
||||
const [ statsError, setStatsError ] = useState(false);
|
||||
@ -52,108 +49,111 @@ export default ({ server, className }: { server: Server; className: string | und
|
||||
alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
|
||||
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
|
||||
}
|
||||
const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited";
|
||||
const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited";
|
||||
const disklimit = server.limits.disk !== 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : 'Unlimited';
|
||||
const memorylimit = server.limits.memory !== 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<Link to={`/server/${server.id}`} className={`grey-row-box cursor-pointer ${className}`}>
|
||||
<GreyRowBox as={Link} to={`/server/${server.id}`}>
|
||||
<div className={'icon'}>
|
||||
<FontAwesomeIcon icon={faServer}/>
|
||||
</div>
|
||||
<div className={'flex-1 ml-4'}>
|
||||
<p className={'text-lg'}>{server.name}</p>
|
||||
<div css={tw`flex-1 ml-4`}>
|
||||
<p css={tw`text-lg`}>{server.name}</p>
|
||||
</div>
|
||||
<div className={'w-1/4 overflow-hidden'}>
|
||||
<div className={'flex ml-4'}>
|
||||
<FontAwesomeIcon icon={faEthernet} className={'text-neutral-500'}/>
|
||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
||||
<div css={tw`w-1/4 overflow-hidden`}>
|
||||
<div css={tw`flex ml-4`}>
|
||||
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
|
||||
<p css={tw`text-sm text-neutral-400 ml-2`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.default).map(allocation => (
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'w-1/3 flex items-baseline relative'}>
|
||||
<div css={tw`w-1/3 flex items-baseline relative`}>
|
||||
{!stats ?
|
||||
!statsError ?
|
||||
<SpinnerOverlay size={'tiny'} visible={true} backgroundOpacity={0.25}/>
|
||||
<SpinnerOverlay size={'small'} visible backgroundOpacity={0.25}/>
|
||||
:
|
||||
server.isInstalling ?
|
||||
<div className={'flex-1 text-center'}>
|
||||
<span className={'bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs'}>
|
||||
<div css={tw`flex-1 text-center`}>
|
||||
<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
|
||||
Installing
|
||||
</span>
|
||||
</div>
|
||||
:
|
||||
<div className={'flex-1 text-center'}>
|
||||
<span className={'bg-red-500 rounded px-2 py-1 text-red-100 text-xs'}>
|
||||
<div css={tw`flex-1 text-center`}>
|
||||
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
|
||||
{server.isSuspended ? 'Suspended' : 'Connection Error'}
|
||||
</span>
|
||||
</div>
|
||||
:
|
||||
<React.Fragment>
|
||||
<div className={'flex-1 flex ml-4 justify-center'}>
|
||||
<div css={tw`flex-1 flex ml-4 justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faMicrochip}
|
||||
className={classNames({
|
||||
'text-neutral-500': !alarms.cpu,
|
||||
'text-red-400': alarms.cpu,
|
||||
})}
|
||||
css={[
|
||||
!alarms.cpu && tw`text-neutral-500`,
|
||||
alarms.cpu && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
className={classNames('text-sm ml-2', {
|
||||
'text-neutral-400': !alarms.cpu,
|
||||
'text-white': alarms.cpu,
|
||||
})}
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.cpu && tw`text-neutral-400`,
|
||||
alarms.cpu && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
{stats.cpuUsagePercent} %
|
||||
</p>
|
||||
</div>
|
||||
<div className={'flex-1 ml-4'}>
|
||||
<div className={'flex justify-center'}>
|
||||
<div css={tw`flex-1 ml-4`}>
|
||||
<div css={tw`flex justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faMemory}
|
||||
className={classNames({
|
||||
'text-neutral-500': !alarms.memory,
|
||||
'text-red-400': alarms.memory,
|
||||
})}
|
||||
css={[
|
||||
!alarms.memory && tw`text-neutral-500`,
|
||||
alarms.memory && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
className={classNames('text-sm ml-2', {
|
||||
'text-neutral-400': !alarms.memory,
|
||||
'text-white': alarms.memory,
|
||||
})}
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.memory && tw`text-neutral-400`,
|
||||
alarms.memory && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
{bytesToHuman(stats.memoryUsageInBytes)}
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {memorylimit}</p>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memorylimit}</p>
|
||||
</div>
|
||||
<div className={'flex-1 ml-4'}>
|
||||
<div className={'flex justify-center'}>
|
||||
<div css={tw`flex-1 ml-4`}>
|
||||
<div css={tw`flex justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faHdd}
|
||||
className={classNames({
|
||||
'text-neutral-500': !alarms.disk,
|
||||
'text-red-400': alarms.disk,
|
||||
})}
|
||||
css={[
|
||||
!alarms.disk && tw`text-neutral-500`,
|
||||
alarms.disk && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
className={classNames('text-sm ml-2', {
|
||||
'text-neutral-400': !alarms.disk,
|
||||
'text-white': alarms.disk,
|
||||
})}
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.disk && tw`text-neutral-400`,
|
||||
alarms.disk && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
{bytesToHuman(stats.diskUsageInBytes)}
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {disklimit}</p>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {disklimit}</p>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
</GreyRowBox>
|
||||
);
|
||||
};
|
||||
|
@ -3,6 +3,8 @@ import { useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
|
||||
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
export default () => {
|
||||
const user = useStoreState((state: ApplicationStore) => state.user.data!);
|
||||
@ -12,43 +14,45 @@ export default () => {
|
||||
<div>
|
||||
{visible &&
|
||||
<DisableTwoFactorModal
|
||||
appear={true}
|
||||
appear
|
||||
visible={visible}
|
||||
onDismissed={() => setVisible(false)}
|
||||
/>
|
||||
}
|
||||
<p className={'text-sm'}>
|
||||
<p css={tw`text-sm`}>
|
||||
Two-factor authentication is currently enabled on your account.
|
||||
</p>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
<div css={tw`mt-6`}>
|
||||
<Button
|
||||
color={'red'}
|
||||
isSecondary
|
||||
onClick={() => setVisible(true)}
|
||||
className={'btn btn-red btn-secondary btn-sm'}
|
||||
>
|
||||
Disable
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
{visible &&
|
||||
<SetupTwoFactorModal
|
||||
appear={true}
|
||||
appear
|
||||
visible={visible}
|
||||
onDismissed={() => setVisible(false)}
|
||||
/>
|
||||
}
|
||||
<p className={'text-sm'}>
|
||||
<p css={tw`text-sm`}>
|
||||
You do not currently have two-factor authentication enabled on your account. Click
|
||||
the button below to begin configuring it.
|
||||
</p>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
<div css={tw`mt-6`}>
|
||||
<Button
|
||||
color={'green'}
|
||||
isSecondary
|
||||
onClick={() => setVisible(true)}
|
||||
className={'btn btn-green btn-secondary btn-sm'}
|
||||
>
|
||||
Begin Setup
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
;
|
||||
|
@ -9,6 +9,9 @@ import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { ApiKey } from '@/api/account/getApiKeys';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Input, { Textarea } from '@/components/elements/Input';
|
||||
|
||||
interface Values {
|
||||
description: string;
|
||||
@ -44,22 +47,21 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||
closeOnEscape={false}
|
||||
closeOnBackground={false}
|
||||
>
|
||||
<h3 className={'mb-6'}>Your API Key</h3>
|
||||
<p className={'text-sm mb-6'}>
|
||||
<h3 css={tw`mb-6`}>Your API Key</h3>
|
||||
<p css={tw`text-sm mb-6`}>
|
||||
The API key you have requested is shown below. Please store this in a safe location, it will not be
|
||||
shown again.
|
||||
</p>
|
||||
<pre className={'text-sm bg-neutral-900 rounded py-2 px-4 font-mono'}>
|
||||
<code className={'font-mono'}>{apiKey}</code>
|
||||
<pre css={tw`text-sm bg-neutral-900 rounded py-2 px-4 font-mono`}>
|
||||
<code css={tw`font-mono`}>{apiKey}</code>
|
||||
</pre>
|
||||
<div className={'flex justify-end mt-6'}>
|
||||
<button
|
||||
<div css={tw`flex justify-end mt-6`}>
|
||||
<Button
|
||||
type={'button'}
|
||||
className={'btn btn-secondary btn-sm'}
|
||||
onClick={() => setApiKey('')}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Formik
|
||||
@ -80,25 +82,19 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||
label={'Description'}
|
||||
name={'description'}
|
||||
description={'A description of this API key.'}
|
||||
className={'mb-6'}
|
||||
css={tw`mb-6`}
|
||||
>
|
||||
<Field name={'description'} className={'input-dark'}/>
|
||||
<Field name={'description'} as={Input}/>
|
||||
</FormikFieldWrapper>
|
||||
<FormikFieldWrapper
|
||||
label={'Allowed IPs'}
|
||||
name={'allowedIps'}
|
||||
description={'Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line.'}
|
||||
>
|
||||
<Field
|
||||
as={'textarea'}
|
||||
name={'allowedIps'}
|
||||
className={'input-dark h-32'}
|
||||
/>
|
||||
<Field as={Textarea} name={'allowedIps'} css={tw`h-32`}/>
|
||||
</FormikFieldWrapper>
|
||||
<div className={'flex justify-end mt-6'}>
|
||||
<button className={'btn btn-primary btn-sm'}>
|
||||
Create
|
||||
</button>
|
||||
<div css={tw`flex justify-end mt-6`}>
|
||||
<Button>Create</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
|
@ -8,6 +8,8 @@ import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
interface Values {
|
||||
password: string;
|
||||
@ -45,19 +47,19 @@ export default ({ ...props }: RequiredModalProps) => {
|
||||
{({ isSubmitting, isValid }) => (
|
||||
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
|
||||
<Form className={'mb-0'}>
|
||||
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
|
||||
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'}/>
|
||||
<Field
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
type={'password'}
|
||||
label={'Current Password'}
|
||||
description={'In order to disable two-factor authentication you will need to provide your account password.'}
|
||||
autoFocus={true}
|
||||
autoFocus
|
||||
/>
|
||||
<div className={'mt-6 text-right'}>
|
||||
<button className={'btn btn-red btn-sm'} disabled={!isValid}>
|
||||
<div css={tw`mt-6 text-right`}>
|
||||
<Button disabled={!isValid}>
|
||||
Disable Two-Factor
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
@ -9,6 +9,8 @@ import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import Field from '@/components/elements/Field';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
interface Values {
|
||||
code: string;
|
||||
@ -64,7 +66,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
||||
.matches(/^(\d){6}$/, 'Authenticator code must be 6 digits.'),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting, isValid }) => (
|
||||
{({ isSubmitting }) => (
|
||||
<Modal
|
||||
{...props}
|
||||
onDismissed={dismiss}
|
||||
@ -75,47 +77,47 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
||||
>
|
||||
{recoveryTokens.length > 0 ?
|
||||
<>
|
||||
<h2 className={'mb-4'}>Two-factor authentication enabled</h2>
|
||||
<p className={'text-neutral-300'}>
|
||||
<h2 css={tw`text-2xl mb-4`}>Two-factor authentication enabled</h2>
|
||||
<p css={tw`text-neutral-300`}>
|
||||
Two-factor authentication has been enabled on your account. Should you loose access to
|
||||
this device you'll need to use on of the codes displayed below in order to access your
|
||||
this device you'll need to use on of the codes displayed below in order to access your
|
||||
account.
|
||||
</p>
|
||||
<p className={'text-neutral-300 mt-4'}>
|
||||
<p css={tw`text-neutral-300 mt-4`}>
|
||||
<strong>These codes will not be displayed again.</strong> Please take note of them now
|
||||
by storing them in a secure repository such as a password manager.
|
||||
</p>
|
||||
<pre className={'mt-4 rounded font-mono bg-neutral-900 p-4'}>
|
||||
{recoveryTokens.map(token => <code key={token} className={'block mb-1'}>{token}</code>)}
|
||||
<pre css={tw`text-sm mt-4 rounded font-mono bg-neutral-900 p-4`}>
|
||||
{recoveryTokens.map(token => <code key={token} css={tw`block mb-1`}>{token}</code>)}
|
||||
</pre>
|
||||
<div className={'text-right'}>
|
||||
<button className={'mt-6 btn btn-lg btn-primary'} onClick={dismiss}>
|
||||
<div css={tw`text-right`}>
|
||||
<Button css={tw`mt-6`} size={'large'} onClick={dismiss}>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<Form className={'mb-0'}>
|
||||
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
|
||||
<div className={'flex flex-wrap'}>
|
||||
<div className={'w-full md:flex-1'}>
|
||||
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
|
||||
<Form css={tw`mb-0`}>
|
||||
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'}/>
|
||||
<div css={tw`flex flex-wrap`}>
|
||||
<div css={tw`w-full md:flex-1`}>
|
||||
<div css={tw`w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto`}>
|
||||
{!token || !token.length ?
|
||||
<img
|
||||
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
|
||||
className={'w-64 h-64 rounded'}
|
||||
css={tw`w-64 h-64 rounded`}
|
||||
/>
|
||||
:
|
||||
<img
|
||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
|
||||
onLoad={() => setLoading(false)}
|
||||
className={'w-full h-full shadow-none rounded-0'}
|
||||
css={tw`w-full h-full shadow-none rounded-none`}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
|
||||
<div className={'flex-1'}>
|
||||
<div css={tw`w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col`}>
|
||||
<div css={tw`flex-1`}>
|
||||
<Field
|
||||
id={'code'}
|
||||
name={'code'}
|
||||
@ -125,10 +127,10 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
||||
autoFocus={!loading}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6 md:mt-0 text-right'}>
|
||||
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
|
||||
<div css={tw`mt-6 md:mt-0 text-right`}>
|
||||
<Button>
|
||||
Setup
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,8 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
interface Values {
|
||||
email: string;
|
||||
@ -54,14 +56,14 @@ export default () => {
|
||||
({ isSubmitting, isValid }) => (
|
||||
<React.Fragment>
|
||||
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
|
||||
<Form className={'m-0'}>
|
||||
<Form css={tw`m-0`}>
|
||||
<Field
|
||||
id={'current_email'}
|
||||
type={'email'}
|
||||
name={'email'}
|
||||
label={'Email'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
id={'confirm_password'}
|
||||
type={'password'}
|
||||
@ -69,10 +71,10 @@ export default () => {
|
||||
label={'Confirm Password'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button className={'btn btn-sm btn-primary'} disabled={isSubmitting || !isValid}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
||||
Update Email
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
|
@ -7,6 +7,8 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import updateAccountPassword from '@/api/account/updateAccountPassword';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
interface Values {
|
||||
current: string;
|
||||
@ -30,7 +32,7 @@ export default () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const submit = (values: Values, { resetForm, setSubmitting }: FormikHelpers<Values>) => {
|
||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('account:password');
|
||||
updateAccountPassword({ ...values })
|
||||
.then(() => {
|
||||
@ -57,14 +59,14 @@ export default () => {
|
||||
({ isSubmitting, isValid }) => (
|
||||
<React.Fragment>
|
||||
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
|
||||
<Form className={'m-0'}>
|
||||
<Form css={tw`m-0`}>
|
||||
<Field
|
||||
id={'current_password'}
|
||||
type={'password'}
|
||||
name={'current'}
|
||||
label={'Current Password'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
id={'new_password'}
|
||||
type={'password'}
|
||||
@ -73,7 +75,7 @@ export default () => {
|
||||
description={'Your new password should be at least 8 characters in length and unique to this website.'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
id={'confirm_password'}
|
||||
type={'password'}
|
||||
@ -81,10 +83,10 @@ export default () => {
|
||||
label={'Confirm New Password'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button className={'btn btn-primary btn-sm'} disabled={isSubmitting || !isValid}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
||||
Update Password
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import useEventListener from '@/plugins/useEventListener';
|
||||
import SearchModal from '@/components/dashboard/search/SearchModal';
|
||||
|
||||
@ -19,7 +19,7 @@ export default () => {
|
||||
<>
|
||||
{visible &&
|
||||
<SearchModal
|
||||
appear={true}
|
||||
appear
|
||||
visible={visible}
|
||||
onDismissed={() => setVisible(false)}
|
||||
/>
|
||||
|
@ -3,7 +3,7 @@ import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||
import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { object, string } from 'yup';
|
||||
import { debounce } from 'lodash-es';
|
||||
import debounce from 'debounce';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
import InputSpinner from '@/components/elements/InputSpinner';
|
||||
import getServers from '@/api/getServers';
|
||||
@ -11,7 +11,9 @@ import { Server } from '@/api/server/getServer';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
import Input from '@/components/elements/Input';
|
||||
|
||||
type Props = RequiredModalProps;
|
||||
|
||||
@ -20,8 +22,7 @@ interface Values {
|
||||
}
|
||||
|
||||
const ServerResult = styled(Link)`
|
||||
${tw`flex items-center bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline`};
|
||||
transition: all 250ms linear;
|
||||
${tw`flex items-center bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline transition-all duration-150`};
|
||||
|
||||
&:hover {
|
||||
${tw`shadow border-cyan-500`};
|
||||
@ -55,6 +56,7 @@ export default ({ ...props }: Props) => {
|
||||
setLoading(true);
|
||||
setSubmitting(false);
|
||||
clearFlashes('search');
|
||||
|
||||
getServers(term)
|
||||
.then(servers => setServers(servers.items.filter((_, index) => index < 5)))
|
||||
.catch(error => {
|
||||
@ -93,16 +95,12 @@ export default ({ ...props }: Props) => {
|
||||
>
|
||||
<SearchWatcher/>
|
||||
<InputSpinner visible={loading}>
|
||||
<Field
|
||||
innerRef={ref}
|
||||
name={'term'}
|
||||
className={'input-dark'}
|
||||
/>
|
||||
<Field as={Input} innerRef={ref} name={'term'}/>
|
||||
</InputSpinner>
|
||||
</FormikFieldWrapper>
|
||||
</Form>
|
||||
{servers.length > 0 &&
|
||||
<div className={'mt-6'}>
|
||||
<div css={tw`mt-6`}>
|
||||
{
|
||||
servers.map(server => (
|
||||
<ServerResult
|
||||
@ -111,17 +109,17 @@ export default ({ ...props }: Props) => {
|
||||
onClick={() => props.onDismissed()}
|
||||
>
|
||||
<div>
|
||||
<p className={'text-sm'}>{server.name}</p>
|
||||
<p className={'mt-1 text-xs text-neutral-400'}>
|
||||
<p css={tw`text-sm`}>{server.name}</p>
|
||||
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.default).map(allocation => (
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div className={'flex-1 text-right'}>
|
||||
<span className={'text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded'}>
|
||||
<div css={tw`flex-1 text-right`}>
|
||||
<span css={tw`text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded`}>
|
||||
{server.node}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState, lazy } from 'react';
|
||||
import useRouter from 'use-react-router';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import ace, { Editor } from 'brace';
|
||||
import getFileContents from '@/api/server/files/getFileContents';
|
||||
import styled from 'styled-components';
|
||||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
import Select from '@/components/elements/Select';
|
||||
// @ts-ignore
|
||||
import modes from '@/modes';
|
||||
|
||||
// @ts-ignore
|
||||
require('brace/ext/modelist');
|
||||
@ -11,7 +12,7 @@ require('ayu-ace/mirage');
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
min-height: 16rem;
|
||||
height: calc(100vh - 16rem);
|
||||
height: calc(100vh - 20rem);
|
||||
${tw`relative`};
|
||||
|
||||
#editor {
|
||||
@ -19,35 +20,6 @@ const EditorContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const modes: { [k: string]: string } = {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
assembly_x86: 'Assembly (x86)',
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
c_cpp: 'C++',
|
||||
coffee: 'Coffeescript',
|
||||
css: 'CSS',
|
||||
dockerfile: 'Dockerfile',
|
||||
golang: 'Go',
|
||||
html: 'HTML',
|
||||
ini: 'Ini',
|
||||
java: 'Java',
|
||||
javascript: 'Javascript',
|
||||
json: 'JSON',
|
||||
kotlin: 'Kotlin',
|
||||
lua: 'Luascript',
|
||||
perl: 'Perl',
|
||||
php: 'PHP',
|
||||
properties: 'Properties',
|
||||
python: 'Python',
|
||||
ruby: 'Ruby',
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
plain_text: 'Plaintext',
|
||||
toml: 'TOML',
|
||||
typescript: 'Typescript',
|
||||
xml: 'XML',
|
||||
yaml: 'YAML',
|
||||
};
|
||||
|
||||
Object.keys(modes).forEach(mode => require(`brace/mode/${mode}`));
|
||||
|
||||
export interface Props {
|
||||
@ -70,7 +42,7 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
|
||||
|
||||
useEffect(() => {
|
||||
editor && editor.session.setMode(mode);
|
||||
}, [editor, mode]);
|
||||
}, [ editor, mode ]);
|
||||
|
||||
useEffect(() => {
|
||||
editor && editor.session.setValue(initialContent || '');
|
||||
@ -113,19 +85,18 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
|
||||
return (
|
||||
<EditorContainer style={style}>
|
||||
<div id={'editor'} ref={ref}/>
|
||||
<div className={'absolute pin-r pin-b z-50'}>
|
||||
<div className={'m-3 rounded bg-neutral-900 border border-black'}>
|
||||
<select
|
||||
className={'input-dark'}
|
||||
<div css={tw`absolute right-0 bottom-0 z-50`}>
|
||||
<div css={tw`m-3 rounded bg-neutral-900 border border-black`}>
|
||||
<Select
|
||||
value={mode.split('/').pop()}
|
||||
onChange={e => setMode(`ace/mode/${e.currentTarget.value}`)}
|
||||
>
|
||||
{
|
||||
Object.keys(modes).map(key => (
|
||||
<option key={key} value={key}>{modes[key]}</option>
|
||||
<option key={key} value={key}>{(modes as { [k: string]: string })[key]}</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</EditorContainer>
|
||||
|
@ -1,20 +1,99 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
||||
type Props = { isLoading?: boolean } & React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
size?: 'xsmall' | 'small' | 'large' | 'xlarge';
|
||||
color?: 'green' | 'red' | 'primary' | 'grey';
|
||||
isSecondary?: boolean;
|
||||
}
|
||||
|
||||
export default ({ isLoading, children, className, ...props }: Props) => (
|
||||
<button
|
||||
{...props}
|
||||
className={classNames('btn btn-sm relative', className)}
|
||||
>
|
||||
const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
||||
${tw`relative inline-block rounded p-2 uppercase tracking-wide text-sm transition-all duration-150 border`};
|
||||
|
||||
${props => ((!props.isSecondary && !props.color) || props.color === 'primary') && css<Props>`
|
||||
${props => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${tw`bg-primary-600 border-primary-700`};
|
||||
}
|
||||
`};
|
||||
|
||||
${props => props.color === 'grey' && css`
|
||||
${tw`border-neutral-600 bg-neutral-500 text-neutral-50`};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${tw`bg-neutral-600 border-neutral-700`};
|
||||
}
|
||||
`};
|
||||
|
||||
${props => props.color === 'green' && css<Props>`
|
||||
${tw`border-green-600 bg-green-500 text-green-50`};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${tw`bg-green-600 border-green-700`};
|
||||
}
|
||||
|
||||
${props => props.isSecondary && css`
|
||||
&:active:not(:disabled) {
|
||||
${tw`bg-green-600 border-green-700`};
|
||||
}
|
||||
`};
|
||||
`};
|
||||
|
||||
${props => props.color === 'red' && css<Props>`
|
||||
${tw`border-red-600 bg-red-500 text-red-50`};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${tw`bg-red-600 border-red-700`};
|
||||
}
|
||||
|
||||
${props => props.isSecondary && css`
|
||||
&:active:not(:disabled) {
|
||||
${tw`bg-red-600 border-red-700`};
|
||||
}
|
||||
`};
|
||||
`};
|
||||
|
||||
${props => props.size === 'xsmall' && tw`p-2 text-xs`};
|
||||
${props => (!props.size || props.size === 'small') && tw`p-3`};
|
||||
${props => props.size === 'large' && tw`p-4 text-sm`};
|
||||
${props => props.size === 'xlarge' && tw`p-4 w-full`};
|
||||
|
||||
${props => props.isSecondary && css<Props>`
|
||||
${tw`border-neutral-600 bg-transparent text-neutral-200`};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${tw`border-neutral-500 text-neutral-100`};
|
||||
${props => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
|
||||
${props => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`};
|
||||
${props => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
|
||||
}
|
||||
`};
|
||||
|
||||
&:disabled { opacity: 0.55; cursor: default }
|
||||
`;
|
||||
|
||||
type ComponentProps = Omit<JSX.IntrinsicElements['button'], 'ref' | keyof Props> & Props;
|
||||
|
||||
const Button: React.FC<ComponentProps> = ({ children, isLoading, ...props }) => (
|
||||
<ButtonStyle {...props}>
|
||||
{isLoading &&
|
||||
<div className={'w-full flex absolute justify-center'} style={{ marginLeft: '-0.75rem' }}>
|
||||
<div className={'spinner-circle spinner-white spinner-sm'}/>
|
||||
<div css={tw`flex absolute justify-center items-center w-full h-full left-0 top-0`}>
|
||||
<Spinner size={'small'}/>
|
||||
</div>
|
||||
}
|
||||
<span className={isLoading ? 'text-transparent' : undefined}>
|
||||
<span css={isLoading ? tw`text-transparent` : undefined}>
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
</ButtonStyle>
|
||||
);
|
||||
|
||||
type LinkProps = Omit<JSX.IntrinsicElements['a'], 'ref' | keyof Props> & Props;
|
||||
|
||||
const LinkButton: React.FC<LinkProps> = props => <ButtonStyle as={'a'} {...props}/>;
|
||||
|
||||
export { LinkButton, ButtonStyle };
|
||||
export default Button;
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Field, FieldProps } from 'formik';
|
||||
import Input from '@/components/elements/Input';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type OmitFields = 'name' | 'value' | 'type' | 'checked' | 'onChange';
|
||||
type OmitFields = 'ref' | 'name' | 'value' | 'type' | 'checked' | 'onClick' | 'onChange';
|
||||
|
||||
type InputProps = Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, OmitFields>;
|
||||
type InputProps = Omit<JSX.IntrinsicElements['input'], OmitFields>;
|
||||
|
||||
const Checkbox = ({ name, value, ...props }: Props & InputProps) => (
|
||||
<Field name={name}>
|
||||
@ -20,7 +21,7 @@ const Checkbox = ({ name, value, ...props }: Props & InputProps) => (
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
<Input
|
||||
{...field}
|
||||
{...props}
|
||||
type={'checkbox'}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@ -16,15 +18,15 @@ const ConfirmationModal = ({ title, appear, children, visible, buttonText, onCon
|
||||
showSpinnerOverlay={showSpinnerOverlay}
|
||||
onDismissed={() => onDismissed()}
|
||||
>
|
||||
<h3 className={'mb-6'}>{title}</h3>
|
||||
<p className={'text-sm'}>{children}</p>
|
||||
<div className={'flex items-center justify-end mt-8'}>
|
||||
<button className={'btn btn-secondary btn-sm'} onClick={() => onDismissed()}>
|
||||
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
|
||||
<p css={tw`text-sm`}>{children}</p>
|
||||
<div css={tw`flex items-center justify-end mt-8`}>
|
||||
<Button isSecondary onClick={() => onDismissed()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className={'btn btn-red btn-sm ml-4'} onClick={() => onConfirmed()}>
|
||||
</Button>
|
||||
<Button color={'red'} css={tw`ml-4`} onClick={() => onConfirmed()}>
|
||||
{buttonText}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
title?: string;
|
||||
@ -12,16 +12,19 @@ type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElemen
|
||||
|
||||
const ContentBox = ({ title, borderColor, showFlashes, showLoadingOverlay, children, ...props }: Props) => (
|
||||
<div {...props}>
|
||||
{title && <h2 className={'text-neutral-300 mb-4 px-4'}>{title}</h2>}
|
||||
{title && <h2 css={tw`text-neutral-300 mb-4 px-4 text-2xl`}>{title}</h2>}
|
||||
{showFlashes &&
|
||||
<FlashMessageRender
|
||||
byKey={typeof showFlashes === 'string' ? showFlashes : undefined}
|
||||
className={'mb-4'}
|
||||
css={tw`mb-4`}
|
||||
/>
|
||||
}
|
||||
<div className={classNames('bg-neutral-700 p-4 rounded shadow-lg relative', borderColor, {
|
||||
'border-t-4': !!borderColor,
|
||||
})}>
|
||||
<div
|
||||
css={[
|
||||
tw`bg-neutral-700 p-4 rounded shadow-lg relative`,
|
||||
!!borderColor && tw`border-t-4`,
|
||||
]}
|
||||
>
|
||||
<SpinnerOverlay visible={showLoadingOverlay || false}/>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
import { breakpoint } from 'styled-components-breakpoint';
|
||||
import styled from 'styled-components/macro';
|
||||
import { breakpoint } from '@/theme';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
max-width: 1200px;
|
||||
@ -9,5 +10,6 @@ const ContentContainer = styled.div`
|
||||
${tw`mx-auto`};
|
||||
`};
|
||||
`;
|
||||
ContentContainer.displayName = 'ContentContainer';
|
||||
|
||||
export default ContentContainer;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import styled from 'styled-components';
|
||||
import React, { createRef } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
@ -12,76 +13,95 @@ export const DropdownButtonRow = styled.button<{ danger?: boolean }>`
|
||||
transition: 150ms all ease;
|
||||
|
||||
&:hover {
|
||||
${props => props.danger
|
||||
? tw`text-red-700 bg-red-100`
|
||||
: tw`text-neutral-700 bg-neutral-100`
|
||||
};
|
||||
${props => props.danger ? tw`text-red-700 bg-red-100` : tw`text-neutral-700 bg-neutral-100`};
|
||||
}
|
||||
`;
|
||||
|
||||
const DropdownMenu = ({ renderToggle, children }: Props) => {
|
||||
const menu = useRef<HTMLDivElement>(null);
|
||||
const [ posX, setPosX ] = useState(0);
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
interface State {
|
||||
posX: number;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const onClickHandler = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
class DropdownMenu extends React.PureComponent<Props, State> {
|
||||
menu = createRef<HTMLDivElement>();
|
||||
|
||||
state: State = {
|
||||
posX: 0,
|
||||
visible: false,
|
||||
};
|
||||
|
||||
componentWillUnmount () {
|
||||
this.removeListeners();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||
const menu = this.menu.current;
|
||||
|
||||
if (this.state.visible && !prevState.visible && menu) {
|
||||
document.addEventListener('click', this.windowListener);
|
||||
document.addEventListener('contextmenu', this.contextMenuListener);
|
||||
menu.setAttribute(
|
||||
'style', `left: ${Math.round(this.state.posX - menu.clientWidth)}px`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.state.visible && prevState.visible) {
|
||||
this.removeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
removeListeners = () => {
|
||||
document.removeEventListener('click', this.windowListener);
|
||||
document.removeEventListener('contextmenu', this.contextMenuListener);
|
||||
};
|
||||
|
||||
onClickHandler = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
|
||||
!visible && setPosX(e.clientX);
|
||||
setVisible(s => !s);
|
||||
this.triggerMenu(e.clientX);
|
||||
};
|
||||
|
||||
const windowListener = (e: MouseEvent) => {
|
||||
if (e.button === 2 || !visible || !menu.current) {
|
||||
contextMenuListener = () => this.setState({ visible: false });
|
||||
|
||||
windowListener = (e: MouseEvent) => {
|
||||
const menu = this.menu.current;
|
||||
|
||||
if (e.button === 2 || !this.state.visible || !menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target === menu.current || menu.current.contains(e.target as Node)) {
|
||||
if (e.target === menu || menu.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target !== menu.current && !menu.current.contains(e.target as Node)) {
|
||||
setVisible(false);
|
||||
if (e.target !== menu && !menu.contains(e.target as Node)) {
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !menu.current) {
|
||||
return;
|
||||
}
|
||||
triggerMenu = (posX: number) => this.setState(s => ({
|
||||
posX: !s.visible ? posX : s.posX,
|
||||
visible: !s.visible,
|
||||
}));
|
||||
|
||||
document.addEventListener('click', windowListener);
|
||||
menu.current.setAttribute(
|
||||
'style', `left: ${Math.round(posX - menu.current.clientWidth)}px`,
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
{this.props.renderToggle(this.onClickHandler)}
|
||||
<Fade timeout={150} in={this.state.visible} unmountOnExit>
|
||||
<div
|
||||
ref={this.menu}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
this.setState({ visible: false });
|
||||
}}
|
||||
css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Fade>
|
||||
</div>
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', windowListener);
|
||||
}
|
||||
}, [ visible ]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderToggle(onClickHandler)}
|
||||
<CSSTransition
|
||||
timeout={250}
|
||||
in={visible}
|
||||
unmountOnExit={true}
|
||||
classNames={'fade'}
|
||||
>
|
||||
<div
|
||||
ref={menu}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setVisible(false);
|
||||
}}
|
||||
className={'absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48'}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default DropdownMenu;
|
||||
|
43
resources/scripts/components/elements/Fade.tsx
Normal file
43
resources/scripts/components/elements/Fade.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
import CSSTransition, { CSSTransitionProps } from 'react-transition-group/CSSTransition';
|
||||
|
||||
interface Props extends Omit<CSSTransitionProps, 'timeout' | 'classNames'> {
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
const Container = styled.div<{ timeout: number }>`
|
||||
.fade-enter, .fade-exit {
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.fade-enter {
|
||||
${tw`opacity-0`};
|
||||
|
||||
&.fade-enter-active {
|
||||
${tw`opacity-100 transition-opacity ease-in`};
|
||||
transition-duration: ${props => props.timeout}ms;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-exit {
|
||||
${tw`opacity-100`};
|
||||
|
||||
&.fade-exit-active {
|
||||
${tw`opacity-0 transition-opacity ease-in`};
|
||||
transition-duration: ${props => props.timeout}ms;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Fade: React.FC<Props> = ({ timeout, children, ...props }) => (
|
||||
<Container timeout={timeout}>
|
||||
<CSSTransition timeout={timeout} classNames={'fade'} {...props}>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
</Container>
|
||||
);
|
||||
Fade.displayName = 'Fade';
|
||||
|
||||
export default Fade;
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { Field as FormikField, FieldProps } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
import Input from '@/components/elements/Input';
|
||||
import Label from '@/components/elements/Label';
|
||||
|
||||
interface OwnProps {
|
||||
name: string;
|
||||
@ -12,21 +13,20 @@ interface OwnProps {
|
||||
|
||||
type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>;
|
||||
|
||||
const Field = ({ id, name, light = false, label, description, validate, className, ...props }: Props) => (
|
||||
<FormikField name={name} validate={validate}>
|
||||
const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, label, description, validate, ...props }, ref) => (
|
||||
<FormikField innerRef={ref} name={name} validate={validate}>
|
||||
{
|
||||
({ field, form: { errors, touched } }: FieldProps) => (
|
||||
<React.Fragment>
|
||||
<>
|
||||
{label &&
|
||||
<label htmlFor={id} className={light ? undefined : 'input-dark-label'}>{label}</label>
|
||||
<Label htmlFor={id} isLight={light}>{label}</Label>
|
||||
}
|
||||
<input
|
||||
<Input
|
||||
id={id}
|
||||
{...field}
|
||||
{...props}
|
||||
className={classNames((className || (light ? 'input' : 'input-dark')), {
|
||||
error: touched[field.name] && errors[field.name],
|
||||
})}
|
||||
isLight={light}
|
||||
hasError={!!(touched[field.name] && errors[field.name])}
|
||||
/>
|
||||
{touched[field.name] && errors[field.name] ?
|
||||
<p className={'input-help error'}>
|
||||
@ -35,10 +35,11 @@ const Field = ({ id, name, light = false, label, description, validate, classNam
|
||||
:
|
||||
description ? <p className={'input-help'}>{description}</p> : null
|
||||
}
|
||||
</React.Fragment>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</FormikField>
|
||||
);
|
||||
));
|
||||
Field.displayName = 'Field';
|
||||
|
||||
export default Field;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Field, FieldProps } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
import InputError from '@/components/elements/InputError';
|
||||
import Label from '@/components/elements/Label';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
@ -17,11 +17,11 @@ const FormikFieldWrapper = ({ id, name, label, className, description, validate,
|
||||
<Field name={name} validate={validate}>
|
||||
{
|
||||
({ field, form: { errors, touched } }: FieldProps) => (
|
||||
<div className={classNames(className, { 'has-error': touched[field.name] && errors[field.name] })}>
|
||||
{label && <label htmlFor={id} className={'input-dark-label'}>{label}</label>}
|
||||
<div className={`${className} ${(touched[field.name] && errors[field.name]) ? 'has-error' : undefined}`}>
|
||||
{label && <Label htmlFor={id}>{label}</Label>}
|
||||
{children}
|
||||
<InputError errors={errors} touched={touched} name={field.name}>
|
||||
{description ? <p className={'input-help'}>{description}</p> : null}
|
||||
{description || null}
|
||||
</InputError>
|
||||
</div>
|
||||
)
|
||||
|
12
resources/scripts/components/elements/GreyRowBox.tsx
Normal file
12
resources/scripts/components/elements/GreyRowBox.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
export default styled.div<{ $hoverable?: boolean }>`
|
||||
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150`};
|
||||
|
||||
${props => props.$hoverable !== false && tw`hover:border-neutral-500`};
|
||||
|
||||
& > div.icon {
|
||||
${tw`rounded-full bg-neutral-500 p-3`};
|
||||
}
|
||||
`;
|
81
resources/scripts/components/elements/Input.tsx
Normal file
81
resources/scripts/components/elements/Input.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
export interface Props {
|
||||
isLight?: boolean;
|
||||
hasError?: boolean;
|
||||
}
|
||||
|
||||
const light = css<Props>`
|
||||
${tw`bg-white border-neutral-200 text-neutral-800`};
|
||||
&:focus { ${tw`border-primary-400`} }
|
||||
|
||||
&:disabled {
|
||||
${tw`bg-neutral-100 border-neutral-200`};
|
||||
}
|
||||
`;
|
||||
|
||||
const checkboxStyle = css<Props>`
|
||||
${tw`cursor-pointer appearance-none inline-block align-middle select-none flex-shrink-0 w-4 h-4 text-primary-400 border border-neutral-300 rounded-sm`};
|
||||
color-adjust: exact;
|
||||
background-origin: border-box;
|
||||
transition: all 75ms linear, box-shadow 25ms linear;
|
||||
|
||||
&:checked {
|
||||
${tw`border-transparent bg-no-repeat bg-center`};
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
${tw`outline-none border-primary-300`};
|
||||
box-shadow: 0 0 0 1px rgba(9, 103, 210, 0.25);
|
||||
}
|
||||
`;
|
||||
|
||||
const inputStyle = css<Props>`
|
||||
// Reset to normal styling.
|
||||
resize: none;
|
||||
${tw`appearance-none outline-none w-full min-w-0`};
|
||||
${tw`p-3 border rounded text-sm transition-all duration-150`};
|
||||
${tw`bg-neutral-600 border-neutral-500 hover:border-neutral-400 text-neutral-200 shadow-none`};
|
||||
|
||||
& + .input-help {
|
||||
${tw`mt-1 text-xs`};
|
||||
${props => props.hasError ? tw`text-red-400` : tw`text-neutral-400`};
|
||||
}
|
||||
|
||||
&:required, &:invalid {
|
||||
${tw`shadow-none`};
|
||||
}
|
||||
|
||||
&:not(:disabled):not(:read-only):focus {
|
||||
${tw`shadow-md border-primary-400`};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
${tw`opacity-75`};
|
||||
}
|
||||
|
||||
${props => props.isLight && light};
|
||||
${props => props.hasError && tw`text-red-600 border-red-500 hover:border-red-600`};
|
||||
`;
|
||||
|
||||
const Input = styled.input<Props>`
|
||||
&:not([type="checkbox"]):not([type="radio"]) {
|
||||
${inputStyle};
|
||||
}
|
||||
|
||||
&[type="checkbox"], &[type="radio"] {
|
||||
${checkboxStyle};
|
||||
|
||||
&[type="radio"] {
|
||||
${tw`rounded-full`};
|
||||
}
|
||||
}
|
||||
`;
|
||||
const Textarea = styled.textarea<Props>`${inputStyle}`;
|
||||
|
||||
export { Textarea };
|
||||
export default Input;
|
@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import capitalize from 'lodash-es/capitalize';
|
||||
import { FormikErrors, FormikTouched } from 'formik';
|
||||
import tw from 'twin.macro';
|
||||
import { capitalize } from '@/helpers';
|
||||
|
||||
interface Props {
|
||||
errors: FormikErrors<any>;
|
||||
touched: FormikTouched<any>;
|
||||
name: string;
|
||||
children?: React.ReactNode;
|
||||
children?: string | number | null | undefined;
|
||||
}
|
||||
|
||||
const InputError = ({ errors, touched, name, children }: Props) => (
|
||||
touched[name] && errors[name] ?
|
||||
<p className={'input-help error'}>
|
||||
<p css={tw`text-xs text-red-400 pt-2`}>
|
||||
{typeof errors[name] === 'string' ?
|
||||
capitalize(errors[name] as string)
|
||||
:
|
||||
@ -19,9 +20,9 @@ const InputError = ({ errors, touched, name, children }: Props) => (
|
||||
}
|
||||
</p>
|
||||
:
|
||||
<React.Fragment>
|
||||
{children}
|
||||
</React.Fragment>
|
||||
<>
|
||||
{children ? <p css={tw`text-xs text-neutral-400 pt-2`}>{children}</p> : null}
|
||||
</>
|
||||
);
|
||||
|
||||
export default InputError;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user