Merge branch 'develop' into feature/server-mounts
This commit is contained in:
commit
295f09ca43
195 changed files with 5395 additions and 5417 deletions
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
|
name: tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branch-ignore:
|
||||||
|
- 'master'
|
||||||
|
- 'release/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
jobs:
|
jobs:
|
||||||
integration_tests:
|
integration_tests:
|
||||||
|
|
|
@ -3,36 +3,9 @@
|
||||||
namespace Pterodactyl\Contracts\Repository;
|
namespace Pterodactyl\Contracts\Repository;
|
||||||
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
interface AllocationRepositoryInterface extends RepositoryInterface
|
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.
|
* Return all of the unique IPs that exist for a given node.
|
||||||
*
|
*
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace Pterodactyl\Contracts\Repository;
|
namespace Pterodactyl\Contracts\Repository;
|
||||||
|
|
||||||
use Pterodactyl\Models\User;
|
|
||||||
use Pterodactyl\Models\Server;
|
use Pterodactyl\Models\Server;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
@ -107,16 +106,6 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
|
||||||
*/
|
*/
|
||||||
public function getDaemonServiceData(Server $server, bool $refresh = false): array;
|
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.
|
* Return a server by UUID.
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,11 +3,10 @@
|
||||||
namespace Pterodactyl\Http\Controllers\Api\Application\Nodes;
|
namespace Pterodactyl\Http\Controllers\Api\Application\Nodes;
|
||||||
|
|
||||||
use Pterodactyl\Models\Node;
|
use Pterodactyl\Models\Node;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Pterodactyl\Models\Allocation;
|
use Pterodactyl\Models\Allocation;
|
||||||
use Pterodactyl\Services\Allocations\AssignmentService;
|
use Pterodactyl\Services\Allocations\AssignmentService;
|
||||||
use Pterodactyl\Services\Allocations\AllocationDeletionService;
|
use Pterodactyl\Services\Allocations\AllocationDeletionService;
|
||||||
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
|
|
||||||
use Pterodactyl\Transformers\Api\Application\AllocationTransformer;
|
use Pterodactyl\Transformers\Api\Application\AllocationTransformer;
|
||||||
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
|
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
|
||||||
use Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest;
|
use Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest;
|
||||||
|
@ -26,41 +25,32 @@ class AllocationController extends ApplicationApiController
|
||||||
*/
|
*/
|
||||||
private $deletionService;
|
private $deletionService;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $repository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AllocationController constructor.
|
* AllocationController constructor.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService
|
* @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService
|
||||||
* @param \Pterodactyl\Services\Allocations\AllocationDeletionService $deletionService
|
* @param \Pterodactyl\Services\Allocations\AllocationDeletionService $deletionService
|
||||||
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
AssignmentService $assignmentService,
|
AssignmentService $assignmentService,
|
||||||
AllocationDeletionService $deletionService,
|
AllocationDeletionService $deletionService
|
||||||
AllocationRepositoryInterface $repository
|
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->assignmentService = $assignmentService;
|
$this->assignmentService = $assignmentService;
|
||||||
$this->deletionService = $deletionService;
|
$this->deletionService = $deletionService;
|
||||||
$this->repository = $repository;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all of the allocations that exist for a given node.
|
* Return all of the allocations that exist for a given node.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest $request
|
* @param \Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest $request
|
||||||
|
* @param \Pterodactyl\Models\Node $node
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function index(GetAllocationsRequest $request): array
|
public function index(GetAllocationsRequest $request, Node $node): array
|
||||||
{
|
{
|
||||||
$allocations = $this->repository->getPaginatedAllocationsForNode(
|
$allocations = $node->allocations()->paginate(50);
|
||||||
$request->getModel(Node::class)->id, 50
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->fractal->collection($allocations)
|
return $this->fractal->collection($allocations)
|
||||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||||
|
@ -71,32 +61,35 @@ class AllocationController extends ApplicationApiController
|
||||||
* Store new allocations for a given node.
|
* Store new allocations for a given node.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Application\Allocations\StoreAllocationRequest $request
|
* @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\CidrOutOfRangeException
|
||||||
* @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException
|
* @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||||
* @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException
|
* @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||||
* @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
* @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.
|
* Delete a specific allocation from the Panel.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest $request
|
* @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
|
* @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
|
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.
|
* Return an instance of an application transformer.
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
namespace Pterodactyl\Http\Controllers\Api\Client;
|
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||||
|
|
||||||
use Pterodactyl\Models\User;
|
use Pterodactyl\Models\User;
|
||||||
|
use Pterodactyl\Models\Server;
|
||||||
use Pterodactyl\Models\Permission;
|
use Pterodactyl\Models\Permission;
|
||||||
|
use Spatie\QueryBuilder\QueryBuilder;
|
||||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||||
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
|
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
|
||||||
use Pterodactyl\Http\Requests\Api\Client\GetServersRequest;
|
use Pterodactyl\Http\Requests\Api\Client\GetServersRequest;
|
||||||
|
@ -36,32 +38,36 @@ class ClientController extends ClientApiController
|
||||||
*/
|
*/
|
||||||
public function index(GetServersRequest $request): array
|
public function index(GetServersRequest $request): array
|
||||||
{
|
{
|
||||||
// Check for the filter parameter on the request.
|
$user = $request->user();
|
||||||
switch ($request->input('filter')) {
|
$level = $request->getFilterLevel();
|
||||||
case 'all':
|
$transformer = $this->getTransformer(ServerTransformer::class);
|
||||||
$filter = User::FILTER_LEVEL_ALL;
|
|
||||||
break;
|
// Start the query builder and ensure we eager load any requested relationships from the request.
|
||||||
case 'admin':
|
$builder = Server::query()->with($this->getIncludesForTransformer($transformer, ['node']));
|
||||||
$filter = User::FILTER_LEVEL_ADMIN;
|
|
||||||
break;
|
if ($level === User::FILTER_LEVEL_OWNER) {
|
||||||
case 'owner':
|
$builder = $builder->where('owner_id', $request->user()->id);
|
||||||
$filter = User::FILTER_LEVEL_OWNER;
|
}
|
||||||
break;
|
// If set to all, display all servers they can access, including those they access as an
|
||||||
case 'subuser-of':
|
// admin. If set to subuser, only return the servers they can access because they are owner,
|
||||||
default:
|
// or marked as a subuser of the server.
|
||||||
$filter = User::FILTER_LEVEL_SUBUSER;
|
elseif (($level === User::FILTER_LEVEL_ALL && ! $user->root_admin) || $level === User::FILTER_LEVEL_SUBUSER) {
|
||||||
break;
|
$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
|
$builder = QueryBuilder::for($builder)->allowedFilters(
|
||||||
->setSearchTerm($request->input('query'))
|
'uuid', 'name', 'external_id'
|
||||||
->filterUserAccessServers(
|
|
||||||
$request->user(), $filter, config('pterodactyl.paginate.frontend.servers')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->fractal->collection($servers)
|
$servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query());
|
||||||
->transformWith($this->getTransformer(ServerTransformer::class))
|
|
||||||
->toArray();
|
return $this->fractal->transformWith($transformer)->collection($servers)->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -159,7 +159,7 @@ class FileController extends ClientApiController
|
||||||
{
|
{
|
||||||
$this->fileRepository
|
$this->fileRepository
|
||||||
->setServer($server)
|
->setServer($server)
|
||||||
->createDirectory($request->input('name'), $request->input('directory', '/'));
|
->createDirectory($request->input('name'), $request->input('root', '/'));
|
||||||
|
|
||||||
return Response::create('', Response::HTTP_NO_CONTENT);
|
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;
|
namespace Pterodactyl\Http\Controllers\Base;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Models\User;
|
|
||||||
use Pterodactyl\Http\Controllers\Controller;
|
use Pterodactyl\Http\Controllers\Controller;
|
||||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||||
|
|
||||||
|
@ -27,15 +25,10 @@ class IndexController extends Controller
|
||||||
/**
|
/**
|
||||||
* Returns listing of user's servers.
|
* Returns listing of user's servers.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index()
|
||||||
{
|
{
|
||||||
$servers = $this->repository->setSearchTerm($request->input('query'))->filterUserAccessServers(
|
return view('templates/base.core');
|
||||||
$request->user(), User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers')
|
|
||||||
);
|
|
||||||
|
|
||||||
return view('templates/base.core', ['servers' => $servers]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
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()) {
|
if (! $server->isInstalled()) {
|
||||||
|
|
|
@ -4,12 +4,12 @@ namespace Pterodactyl\Http\Middleware\Api\Client;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pterodactyl\Models\Backup;
|
use Pterodactyl\Models\Backup;
|
||||||
|
use Pterodactyl\Models\Database;
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
||||||
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
|
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
|
||||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
|
||||||
|
|
||||||
class SubstituteClientApiBindings extends ApiSubstituteBindings
|
class SubstituteClientApiBindings extends ApiSubstituteBindings
|
||||||
{
|
{
|
||||||
|
@ -43,17 +43,9 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->router->bind('database', function ($value) use ($request) {
|
$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([
|
return Database::query()->where('id', $id)->firstOrFail();
|
||||||
['id', '=', $id],
|
|
||||||
]);
|
|
||||||
} catch (RecordNotFoundException $exception) {
|
|
||||||
$request->attributes->set('is_missing_model', true);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->router->model('backup', Backup::class, function ($value) {
|
$this->router->model('backup', Backup::class, function ($value) {
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Requests\Api\Client;
|
namespace Pterodactyl\Http\Requests\Api\Client;
|
||||||
|
|
||||||
|
use Pterodactyl\Models\User;
|
||||||
|
|
||||||
class GetServersRequest extends ClientApiRequest
|
class GetServersRequest extends ClientApiRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -11,4 +13,28 @@ class GetServersRequest extends ClientApiRequest
|
||||||
{
|
{
|
||||||
return true;
|
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 string|null $ip_alias
|
||||||
* @property int $port
|
* @property int $port
|
||||||
* @property int|null $server_id
|
* @property int|null $server_id
|
||||||
|
* @property string|null $notes
|
||||||
* @property \Carbon\Carbon|null $created_at
|
* @property \Carbon\Carbon|null $created_at
|
||||||
* @property \Carbon\Carbon|null $updated_at
|
* @property \Carbon\Carbon|null $updated_at
|
||||||
*
|
*
|
||||||
|
@ -60,6 +61,7 @@ class Allocation extends Model
|
||||||
'port' => 'required|numeric|between:1024,65553',
|
'port' => 'required|numeric|between:1024,65553',
|
||||||
'ip_alias' => 'nullable|string',
|
'ip_alias' => 'nullable|string',
|
||||||
'server_id' => 'nullable|exists:servers,id',
|
'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_BACKUP_DOWNLOAD = 'backup.download';
|
||||||
|
|
||||||
const ACTION_ALLOCATION_READ = 'allocation.read';
|
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_READ = 'file.read';
|
||||||
const ACTION_FILE_CREATE = 'file.create';
|
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.',
|
'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
|
||||||
'keys' => [
|
'keys' => [
|
||||||
'read' => 'Allows a user to view the allocations assigned to this server.',
|
'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;
|
const UPDATED_AT = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $timestamps = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -7,6 +7,7 @@ use Illuminate\Support\Collection;
|
||||||
use Illuminate\Validation\Rules\In;
|
use Illuminate\Validation\Rules\In;
|
||||||
use Illuminate\Auth\Authenticatable;
|
use Illuminate\Auth\Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Pterodactyl\Models\Traits\Searchable;
|
use Pterodactyl\Models\Traits\Searchable;
|
||||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||||
use Pterodactyl\Traits\Helpers\AvailableLanguages;
|
use Pterodactyl\Traits\Helpers\AvailableLanguages;
|
||||||
|
@ -260,4 +261,21 @@ class User extends Model implements
|
||||||
{
|
{
|
||||||
return $this->hasMany(RecoveryToken::class);
|
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 Illuminate\Support\ServiceProvider;
|
||||||
use Pterodactyl\Http\ViewComposers\AssetComposer;
|
use Pterodactyl\Http\ViewComposers\AssetComposer;
|
||||||
use Pterodactyl\Http\ViewComposers\ServerListComposer;
|
|
||||||
use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer;
|
|
||||||
|
|
||||||
class ViewComposerServiceProvider extends ServiceProvider
|
class ViewComposerServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@ -15,10 +13,5 @@ class ViewComposerServiceProvider extends ServiceProvider
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
$this->app->make('view')->composer('*', AssetComposer::class);
|
$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 Illuminate\Support\Collection;
|
||||||
use Pterodactyl\Models\Allocation;
|
use Pterodactyl\Models\Allocation;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
|
||||||
|
|
||||||
class AllocationRepository extends EloquentRepository implements AllocationRepositoryInterface
|
class AllocationRepository extends EloquentRepository implements AllocationRepositoryInterface
|
||||||
|
@ -20,41 +19,6 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
|
||||||
return Allocation::class;
|
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.
|
* Return all of the unique IPs that exist for a given node.
|
||||||
*
|
*
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
namespace Pterodactyl\Repositories\Eloquent;
|
namespace Pterodactyl\Repositories\Eloquent;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Webmozart\Assert\Assert;
|
use Webmozart\Assert\Assert;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Pterodactyl\Repositories\Repository;
|
use Pterodactyl\Repositories\Repository;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Query\Expression;
|
use Illuminate\Database\Query\Expression;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
@ -15,6 +17,53 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
|
||||||
|
|
||||||
abstract class EloquentRepository extends Repository implements RepositoryInterface
|
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
|
* Return an instance of the eloquent model bound to this
|
||||||
* repository instance.
|
* repository instance.
|
||||||
|
@ -236,6 +285,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
|
||||||
* Return all records associated with the given model.
|
* Return all records associated with the given model.
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Support\Collection
|
* @return \Illuminate\Support\Collection
|
||||||
|
* @deprecated Just use the model
|
||||||
*/
|
*/
|
||||||
public function all(): Collection
|
public function all(): Collection
|
||||||
{
|
{
|
||||||
|
@ -313,6 +363,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
|
||||||
* Get the amount of entries in the database.
|
* Get the amount of entries in the database.
|
||||||
*
|
*
|
||||||
* @return int
|
* @return int
|
||||||
|
* @deprecated just use the count method off a model
|
||||||
*/
|
*/
|
||||||
public function count(): int
|
public function count(): int
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace Pterodactyl\Repositories\Eloquent;
|
namespace Pterodactyl\Repositories\Eloquent;
|
||||||
|
|
||||||
use Pterodactyl\Models\User;
|
|
||||||
use Pterodactyl\Models\Server;
|
use Pterodactyl\Models\Server;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
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.
|
* 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 ! $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.
|
* Get the amount of servers that are suspended.
|
||||||
*
|
*
|
||||||
|
|
|
@ -270,7 +270,9 @@ class ServerCreationService
|
||||||
$records = array_merge($records, $data['allocation_additional']);
|
$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,
|
'ip' => $allocation->ip,
|
||||||
'alias' => $allocation->ip_alias,
|
'alias' => $allocation->ip_alias,
|
||||||
'port' => $allocation->port,
|
'port' => $allocation->port,
|
||||||
|
'notes' => $allocation->notes,
|
||||||
'assigned' => ! is_null($allocation->server_id),
|
'assigned' => ! is_null($allocation->server_id),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,13 @@ class AllocationTransformer extends BaseClientTransformer
|
||||||
*/
|
*/
|
||||||
public function transform(Allocation $model)
|
public function transform(Allocation $model)
|
||||||
{
|
{
|
||||||
$model->loadMissing('server');
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'id' => $model->id,
|
||||||
'ip' => $model->ip,
|
'ip' => $model->ip,
|
||||||
'alias' => $model->ip_alias,
|
'ip_alias' => $model->ip_alias,
|
||||||
'port' => $model->port,
|
'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\Egg;
|
||||||
use Pterodactyl\Models\Server;
|
use Pterodactyl\Models\Server;
|
||||||
use Pterodactyl\Models\Subuser;
|
use Pterodactyl\Models\Subuser;
|
||||||
|
use Pterodactyl\Models\Allocation;
|
||||||
|
|
||||||
class ServerTransformer extends BaseClientTransformer
|
class ServerTransformer extends BaseClientTransformer
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $defaultIncludes = ['allocations'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
|
@ -41,10 +47,6 @@ class ServerTransformer extends BaseClientTransformer
|
||||||
'port' => $server->node->daemonSFTP,
|
'port' => $server->node->daemonSFTP,
|
||||||
],
|
],
|
||||||
'description' => $server->description,
|
'description' => $server->description,
|
||||||
'allocation' => [
|
|
||||||
'ip' => $server->allocation->alias,
|
|
||||||
'port' => $server->allocation->port,
|
|
||||||
],
|
|
||||||
'limits' => [
|
'limits' => [
|
||||||
'memory' => $server->memory,
|
'memory' => $server->memory,
|
||||||
'swap' => $server->swap,
|
'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.
|
* 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",
|
"psy/psysh": "^0.10.4",
|
||||||
"s1lentium/iptools": "^1.1",
|
"s1lentium/iptools": "^1.1",
|
||||||
"spatie/laravel-fractal": "^5.7",
|
"spatie/laravel-fractal": "^5.7",
|
||||||
|
"spatie/laravel-query-builder": "^2.8",
|
||||||
"staudenmeir/belongs-to-through": "^2.10",
|
"staudenmeir/belongs-to-through": "^2.10",
|
||||||
"symfony/yaml": "^4.4",
|
"symfony/yaml": "^4.4",
|
||||||
"webmozart/assert": "^1.9"
|
"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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "155b8e930e604c0476fa975b1084ca3f",
|
"content-hash": "d05ab995e4aff4b847ff2a027924065c",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "appstract/laravel-blade-directives",
|
"name": "appstract/laravel-blade-directives",
|
||||||
|
@ -3361,6 +3361,70 @@
|
||||||
],
|
],
|
||||||
"time": "2020-03-02T18:40:49+00:00"
|
"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",
|
"name": "staudenmeir/belongs-to-through",
|
||||||
"version": "v2.10",
|
"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",
|
"name": "pterodactyl-panel",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.19",
|
"@fortawesome/fontawesome-svg-core": "1.2.19",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.9.0",
|
"@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",
|
"@types/react-google-recaptcha": "^1.1.1",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.2",
|
||||||
"ayu-ace": "^2.0.4",
|
"ayu-ace": "^2.0.4",
|
||||||
"brace": "^0.11.1",
|
"brace": "^0.11.1",
|
||||||
"chart.js": "^2.8.0",
|
"chart.js": "^2.8.0",
|
||||||
"classnames": "^2.2.6",
|
"date-fns": "^2.14.0",
|
||||||
"date-fns": "^1.29.0",
|
"debounce": "^1.2.0",
|
||||||
"easy-peasy": "^3.2.3",
|
"deepmerge": "^4.2.2",
|
||||||
|
"easy-peasy": "^3.3.1",
|
||||||
"events": "^3.0.0",
|
"events": "^3.0.0",
|
||||||
"formik": "^2.1.4",
|
"formik": "^2.1.4",
|
||||||
"i18next": "^19.0.0",
|
"i18next": "^19.0.0",
|
||||||
|
@ -19,26 +20,26 @@
|
||||||
"i18next-localstorage-backend": "^3.0.0",
|
"i18next-localstorage-backend": "^3.0.0",
|
||||||
"i18next-xhr-backend": "^3.2.2",
|
"i18next-xhr-backend": "^3.2.2",
|
||||||
"jquery": "^3.3.1",
|
"jquery": "^3.3.1",
|
||||||
"lodash-es": "^4.17.15",
|
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"query-string": "^6.7.0",
|
"query-string": "^6.7.0",
|
||||||
"react": "^16.12.0",
|
"react": "^16.13.1",
|
||||||
"react-dom": "npm:@hot-loader/react-dom",
|
"react-dom": "npm:@hot-loader/react-dom",
|
||||||
|
"react-fast-compare": "^3.2.0",
|
||||||
"react-google-recaptcha": "^2.0.1",
|
"react-google-recaptcha": "^2.0.1",
|
||||||
"react-hot-loader": "^4.12.18",
|
"react-hot-loader": "^4.12.21",
|
||||||
"react-i18next": "^11.2.1",
|
"react-i18next": "^11.2.1",
|
||||||
"react-redux": "^7.1.0",
|
"react-redux": "^7.1.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-transition-group": "^4.3.0",
|
"react-transition-group": "^4.4.1",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"styled-components": "^4.4.1",
|
"styled-components": "^5.1.1",
|
||||||
"styled-components-breakpoint": "^3.0.0-preview.20",
|
"styled-components-breakpoint": "^3.0.0-preview.20",
|
||||||
"use-react-router": "^1.0.7",
|
"swr": "^0.2.3",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
"xterm": "^3.14.4",
|
"xterm": "^3.14.4",
|
||||||
"xterm-addon-attach": "^0.1.0",
|
"xterm-addon-attach": "^0.1.0",
|
||||||
"xterm-addon-fit": "^0.1.0",
|
"xterm-addon-fit": "^0.1.0",
|
||||||
"yup": "^0.27.0"
|
"yup": "^0.29.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.7.5",
|
"@babel/core": "^7.7.5",
|
||||||
|
@ -47,75 +48,65 @@
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
|
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||||
|
"@babel/plugin-transform-react-jsx": "^7.10.4",
|
||||||
"@babel/plugin-transform-runtime": "^7.7.5",
|
"@babel/plugin-transform-runtime": "^7.7.5",
|
||||||
"@babel/preset-env": "^7.7.5",
|
"@babel/preset-env": "^7.7.5",
|
||||||
"@babel/preset-react": "^7.7.4",
|
"@babel/preset-react": "^7.7.4",
|
||||||
"@babel/preset-typescript": "^7.7.4",
|
"@babel/preset-typescript": "^7.7.4",
|
||||||
"@babel/runtime": "^7.7.5",
|
"@babel/runtime": "^7.7.5",
|
||||||
"@types/chart.js": "^2.8.5",
|
"@types/chart.js": "^2.8.5",
|
||||||
"@types/classnames": "^2.2.8",
|
"@types/debounce": "^1.2.0",
|
||||||
"@types/events": "^3.0.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/node": "^12.6.9",
|
||||||
"@types/query-string": "^6.3.0",
|
"@types/query-string": "^6.3.0",
|
||||||
"@types/react": "^16.9.15",
|
"@types/react": "^16.9.41",
|
||||||
"@types/react-dom": "^16.9.4",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@types/react-redux": "^7.1.1",
|
"@types/react-redux": "^7.1.1",
|
||||||
"@types/react-router": "^5.1.3",
|
"@types/react-router": "^5.1.3",
|
||||||
"@types/react-router-dom": "^5.1.3",
|
"@types/react-router-dom": "^5.1.3",
|
||||||
"@types/react-transition-group": "^2.9.2",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/styled-components": "^4.4.0",
|
"@types/styled-components": "^5.1.0",
|
||||||
"@types/uuid": "^3.4.5",
|
"@types/uuid": "^3.4.5",
|
||||||
"@types/webpack-env": "^1.13.6",
|
"@types/webpack-env": "^1.15.2",
|
||||||
"@types/yup": "^0.26.17",
|
"@types/yup": "^0.29.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.19.0",
|
"@typescript-eslint/eslint-plugin": "^3.5.0",
|
||||||
"@typescript-eslint/parser": "^2.19.0",
|
"@typescript-eslint/parser": "^3.5.0",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-styled-components": "^1.10.6",
|
"babel-plugin-styled-components": "^1.10.7",
|
||||||
"babel-plugin-tailwind-components": "^0.5.10",
|
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"css-loader": "^3.2.1",
|
"css-loader": "^3.2.1",
|
||||||
"cssnano": "^4.1.10",
|
"eslint": "^7.4.0",
|
||||||
"eslint": "^5.16.0",
|
"eslint-config-standard": "^14.1.1",
|
||||||
"eslint-config-standard": "^12.0.0",
|
"eslint-plugin-import": "^2.22.0",
|
||||||
"eslint-plugin-import": "^2.17.3",
|
|
||||||
"eslint-plugin-node": "^9.1.0",
|
"eslint-plugin-node": "^9.1.0",
|
||||||
"eslint-plugin-promise": "^4.1.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-react-hooks": "^2.1.2",
|
"eslint-plugin-react": "^7.20.3",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-react-hooks": "^4.0.5",
|
||||||
"fork-ts-checker-webpack-plugin": "^1.5.0",
|
"eslint-plugin-standard": "^4.0.1",
|
||||||
"glob-all": "^3.1.0",
|
"fork-ts-checker-webpack-plugin": "^5.0.6",
|
||||||
"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",
|
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-devtools-extension": "^2.13.8",
|
||||||
"resolve-url-loader": "^3.0.0",
|
"source-map-loader": "^1.0.1",
|
||||||
"source-map-loader": "^0.2.4",
|
"style-loader": "^1.2.1",
|
||||||
"style-loader": "^0.23.1",
|
"svg-url-loader": "^6.0.0",
|
||||||
"tailwindcss": "^0.7.4",
|
"tailwindcss": "^1.4.6",
|
||||||
"terser-webpack-plugin": "^1.3.0",
|
"terser-webpack-plugin": "^3.0.6",
|
||||||
"ts-loader": "^6.2.1",
|
"twin.macro": "^1.4.1",
|
||||||
"typescript": "^3.7.5",
|
"typescript": "^3.9.6",
|
||||||
"webpack": "^4.41.2",
|
"typescript-plugin-tw-template": "^2.0.1",
|
||||||
|
"webpack": "^4.43.0",
|
||||||
"webpack-assets-manifest": "^3.1.1",
|
"webpack-assets-manifest": "^3.1.1",
|
||||||
"webpack-cli": "^3.3.10",
|
"webpack-bundle-analyzer": "^3.8.0",
|
||||||
"webpack-dev-server": "^3.9.0",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-manifest-plugin": "^2.0.3",
|
"webpack-dev-server": "^3.11.0",
|
||||||
"yarn-deduplicate": "^1.1.1"
|
"yarn-deduplicate": "^1.1.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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",
|
"watch": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
|
||||||
"build": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --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",
|
"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": [
|
"browserslist": [
|
||||||
"> 0.5%",
|
"> 0.5%",
|
||||||
|
|
|
@ -1,38 +1,76 @@
|
||||||
parser: "@typescript-eslint/parser"
|
parser: "@typescript-eslint/parser"
|
||||||
parserOptions:
|
parserOptions:
|
||||||
ecmaVersion: 6
|
ecmaVersion: 6
|
||||||
|
ecmaFeatures:
|
||||||
|
jsx: true
|
||||||
project: "./tsconfig.json"
|
project: "./tsconfig.json"
|
||||||
tsconfigRootDir: "./"
|
tsconfigRootDir: "./"
|
||||||
|
settings:
|
||||||
|
react:
|
||||||
|
pragma: "React"
|
||||||
|
version: "detect"
|
||||||
|
linkComponents:
|
||||||
|
- name: Link
|
||||||
|
linkAttribute: to
|
||||||
|
- name: NavLink
|
||||||
|
linkAttribute: to
|
||||||
env:
|
env:
|
||||||
browser: true
|
browser: true
|
||||||
es6: true
|
es6: true
|
||||||
plugins:
|
plugins:
|
||||||
- "@typescript-eslint"
|
- "react"
|
||||||
- "react-hooks"
|
- "react-hooks"
|
||||||
|
- "@typescript-eslint"
|
||||||
extends:
|
extends:
|
||||||
- "standard"
|
- "standard"
|
||||||
|
- "plugin:react/recommended"
|
||||||
- "plugin:@typescript-eslint/recommended"
|
- "plugin:@typescript-eslint/recommended"
|
||||||
globals:
|
|
||||||
tw: "readonly"
|
|
||||||
rules:
|
rules:
|
||||||
indent:
|
indent:
|
||||||
- error
|
- error
|
||||||
- 4
|
- 4
|
||||||
|
- SwitchCase: 1
|
||||||
semi:
|
semi:
|
||||||
- error
|
- error
|
||||||
- always
|
- always
|
||||||
comma-dangle:
|
comma-dangle:
|
||||||
- error
|
- error
|
||||||
- always-multiline
|
- always-multiline
|
||||||
|
array-bracket-spacing:
|
||||||
|
- warn
|
||||||
|
- always
|
||||||
"react-hooks/rules-of-hooks":
|
"react-hooks/rules-of-hooks":
|
||||||
- error
|
- error
|
||||||
"react-hooks/exhaustive-deps": 0
|
"react-hooks/exhaustive-deps": 0
|
||||||
"@typescript-eslint/explicit-function-return-type": 0
|
"@typescript-eslint/explicit-function-return-type": 0
|
||||||
"@typescript-eslint/explicit-member-accessibility": 0
|
"@typescript-eslint/explicit-member-accessibility": 0
|
||||||
"@typescript-eslint/ban-ts-ignore": 0
|
"@typescript-eslint/ban-ts-ignore": 0
|
||||||
"@typescript-eslint/no-unused-vars": 0
|
|
||||||
"@typescript-eslint/no-explicit-any": 0
|
"@typescript-eslint/no-explicit-any": 0
|
||||||
"@typescript-eslint/no-non-null-assertion": 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:
|
overrides:
|
||||||
- files:
|
- files:
|
||||||
- "**/*.tsx"
|
- "**/*.tsx"
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route } from 'react-router';
|
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<{
|
const StyledSwitchTransition = styled(SwitchTransition)`
|
||||||
children: React.ReactNode;
|
${tw`relative`};
|
||||||
}>;
|
|
||||||
|
|
||||||
export default ({ children }: Props) => (
|
& section {
|
||||||
|
${tw`absolute w-full top-0 left-0`};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TransitionRouter: React.FC = ({ children }) => (
|
||||||
<Route
|
<Route
|
||||||
render={({ location }) => (
|
render={({ location }) => (
|
||||||
<TransitionGroup className={'route-transition-group'}>
|
<StyledSwitchTransition>
|
||||||
<CSSTransition key={location.key} timeout={250} in={true} appear={true} classNames={'fade'}>
|
<Fade timeout={150} key={location.key} in appear unmountOnExit>
|
||||||
<section>
|
<section>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
</CSSTransition>
|
</Fade>
|
||||||
</TransitionGroup>
|
</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 }> => {
|
export default (description: string, allowedIps: string): Promise<ApiKey & { secretToken: string }> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post(`/api/client/account/api-keys`, {
|
http.post('/api/client/account/api-keys', {
|
||||||
description,
|
description,
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
allowed_ips: allowedIps.length > 0 ? allowedIps.split('\n') : [],
|
allowed_ips: allowedIps.length > 0 ? allowedIps.split('\n') : [],
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve({
|
.then(({ data }) => resolve({
|
||||||
...rawDataToApiKey(data.attributes),
|
...rawDataToApiKey(data.attributes),
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
secretToken: data.meta?.secret_token ?? '',
|
secretToken: data.meta?.secret_token ?? '',
|
||||||
}))
|
}))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
|
|
@ -9,10 +9,8 @@ interface Data {
|
||||||
export default ({ current, password, confirmPassword }: Data): Promise<void> => {
|
export default ({ current, password, confirmPassword }: Data): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.put('/api/client/account/password', {
|
http.put('/api/client/account/password', {
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
current_password: current,
|
current_password: current,
|
||||||
password: password,
|
password: password,
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
password_confirmation: confirmPassword,
|
password_confirmation: confirmPassword,
|
||||||
})
|
})
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
|
|
|
@ -4,11 +4,9 @@ import { LoginResponse } from '@/api/auth/login';
|
||||||
export default (token: string, code: string, recoveryToken?: string): Promise<LoginResponse> => {
|
export default (token: string, code: string, recoveryToken?: string): Promise<LoginResponse> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/auth/login/checkpoint', {
|
http.post('/auth/login/checkpoint', {
|
||||||
/* eslint-disable @typescript-eslint/camelcase */
|
|
||||||
confirmation_token: token,
|
confirmation_token: token,
|
||||||
authentication_code: code,
|
authentication_code: code,
|
||||||
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
|
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
|
||||||
/* eslint-enable @typescript-eslint/camelcase */
|
|
||||||
})
|
})
|
||||||
.then(response => resolve({
|
.then(response => resolve({
|
||||||
complete: response.data.data.complete,
|
complete: response.data.data.complete,
|
||||||
|
|
|
@ -17,7 +17,6 @@ export default (email: string, data: Data): Promise<PasswordResetResponse> => {
|
||||||
email,
|
email,
|
||||||
token: data.token,
|
token: data.token,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
password_confirmation: data.passwordConfirmation,
|
password_confirmation: data.passwordConfirmation,
|
||||||
})
|
})
|
||||||
.then(response => resolve({
|
.then(response => resolve({
|
||||||
|
|
|
@ -3,16 +3,15 @@ import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
||||||
|
|
||||||
export default (query?: string, includeAdmin?: boolean): Promise<PaginatedResult<Server>> => {
|
export default (query?: string, includeAdmin?: boolean): Promise<PaginatedResult<Server>> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client`, {
|
http.get('/api/client', {
|
||||||
params: {
|
params: {
|
||||||
include: [ 'allocation' ],
|
include: [ 'allocation' ],
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
type: includeAdmin ? 'all' : undefined,
|
||||||
filter: includeAdmin ? 'all' : undefined,
|
'filter[name]': query,
|
||||||
query,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve({
|
.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),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
}))
|
}))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import http from '@/api/http';
|
||||||
|
|
||||||
export default (): Promise<PanelPermissions> => {
|
export default (): Promise<PanelPermissions> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/permissions`)
|
http.get('/api/client/permissions')
|
||||||
.then(({ data }) => resolve(data.attributes.permissions))
|
.then(({ data }) => resolve(data.attributes.permissions))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ const http: AxiosInstance = axios.create({
|
||||||
timeout: 20000,
|
timeout: 20000,
|
||||||
headers: {
|
headers: {
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'Accept': 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRF-Token': (window as any).X_CSRF_TOKEN as string || '',
|
'X-CSRF-Token': (window as any).X_CSRF_TOKEN as string || '',
|
||||||
},
|
},
|
||||||
|
@ -75,12 +75,15 @@ export interface FractalResponseData {
|
||||||
object: string;
|
object: string;
|
||||||
attributes: {
|
attributes: {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
relationships?: {
|
relationships?: Record<string, FractalResponseData | FractalResponseList>;
|
||||||
[k: string]: FractalResponseData;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FractalResponseList {
|
||||||
|
object: 'list';
|
||||||
|
data: FractalResponseData[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaginatedResult<T> {
|
export interface PaginatedResult<T> {
|
||||||
items: T[];
|
items: T[];
|
||||||
pagination: PaginationDataSet;
|
pagination: PaginationDataSet;
|
||||||
|
|
|
@ -14,12 +14,12 @@ export interface FileObject {
|
||||||
modifiedAt: Date;
|
modifiedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (uuid: string, directory?: string): Promise<FileObject[]> => {
|
export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
|
||||||
return new Promise((resolve, reject) => {
|
const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
|
||||||
http.get(`/api/client/servers/${uuid}/files/list`, {
|
|
||||||
params: { directory },
|
params: { directory },
|
||||||
})
|
});
|
||||||
.then(response => resolve((response.data.data || []).map((item: any): FileObject => ({
|
|
||||||
|
return (data.data || []).map((item: any): FileObject => ({
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
name: item.attributes.name,
|
name: item.attributes.name,
|
||||||
mode: item.attributes.mode,
|
mode: item.attributes.mode,
|
||||||
|
@ -30,7 +30,5 @@ export default (uuid: string, directory?: string): Promise<FileObject[]> => {
|
||||||
mimetype: item.attributes.mimetype,
|
mimetype: item.attributes.mimetype,
|
||||||
createdAt: new Date(item.attributes.created_at),
|
createdAt: new Date(item.attributes.created_at),
|
||||||
modifiedAt: new Date(item.attributes.modified_at),
|
modifiedAt: new Date(item.attributes.modified_at),
|
||||||
}))))
|
}));
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,9 +8,7 @@ interface Data {
|
||||||
export default (uuid: string, { renameFrom, renameTo }: Data): Promise<void> => {
|
export default (uuid: string, { renameFrom, renameTo }: Data): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.put(`/api/client/servers/${uuid}/files/rename`, {
|
http.put(`/api/client/servers/${uuid}/files/rename`, {
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
rename_from: renameFrom,
|
rename_from: renameFrom,
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
rename_to: renameTo,
|
rename_to: renameTo,
|
||||||
})
|
})
|
||||||
.then(() => resolve())
|
.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 {
|
export interface Allocation {
|
||||||
|
id: number;
|
||||||
ip: string;
|
ip: string;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
port: number;
|
port: number;
|
||||||
default: boolean;
|
notes: string | null;
|
||||||
|
isDefault: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Server {
|
export interface Server {
|
||||||
|
@ -35,7 +38,7 @@ export interface Server {
|
||||||
isInstalling: boolean;
|
isInstalling: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToServerObject = (data: any): Server => ({
|
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
|
||||||
id: data.identifier,
|
id: data.identifier,
|
||||||
uuid: data.uuid,
|
uuid: data.uuid,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
@ -45,24 +48,20 @@ export const rawDataToServerObject = (data: any): Server => ({
|
||||||
port: data.sftp_details.port,
|
port: data.sftp_details.port,
|
||||||
},
|
},
|
||||||
description: data.description ? ((data.description.length > 0) ? data.description : null) : null,
|
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 },
|
limits: { ...data.limits },
|
||||||
featureLimits: { ...data.feature_limits },
|
featureLimits: { ...data.feature_limits },
|
||||||
isSuspended: data.is_suspended,
|
isSuspended: data.is_suspended,
|
||||||
isInstalling: data.is_installing,
|
isInstalling: data.is_installing,
|
||||||
|
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default (uuid: string): Promise<[ Server, string[] ]> => {
|
export default (uuid: string): Promise<[ Server, string[] ]> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${uuid}`)
|
http.get(`/api/client/servers/${uuid}`)
|
||||||
.then(({ data }) => resolve([
|
.then(({ data }) => resolve([
|
||||||
rawDataToServerObject(data.attributes),
|
rawDataToServerObject(data),
|
||||||
data.meta?.is_server_owner ? ['*'] : (data.meta?.user_permissions || []),
|
// eslint-disable-next-line camelcase
|
||||||
|
data.meta?.is_server_owner ? [ '*' ] : (data.meta?.user_permissions || []),
|
||||||
]))
|
]))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,7 @@ export const rawDataToServerDatabase = (data: any): ServerDatabase => ({
|
||||||
password: data.relationships && data.relationships.password ? data.relationships.password.attributes.password : undefined,
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${uuid}/databases`, {
|
http.get(`/api/client/servers/${uuid}/databases`, {
|
||||||
params: includePassword ? { include: 'password' } : undefined,
|
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())
|
.then(() => resolve())
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
|
@ -11,7 +11,6 @@ export default (uuid: string, schedule: number, task: number | undefined, { time
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post(`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`, {
|
http.post(`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`, {
|
||||||
...data,
|
...data,
|
||||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
||||||
time_offset: timeOffset,
|
time_offset: timeOffset,
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve(rawDataToServerTask(data.attributes)))
|
.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}`)
|
http.delete(`/api/client/servers/${uuid}/schedules/${scheduleId}/tasks/${taskId}`)
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default (uuid: string, schedule: number): Promise<Schedule> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, {
|
http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, {
|
||||||
params: {
|
params: {
|
||||||
include: ['tasks'],
|
include: [ 'tasks' ],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
|
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default (uuid: string): Promise<Schedule[]> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${uuid}/schedules`, {
|
http.get(`/api/client/servers/${uuid}/schedules`, {
|
||||||
params: {
|
params: {
|
||||||
include: ['tasks'],
|
include: [ 'tasks' ],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve((data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes))))
|
.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)))
|
.then(data => resolve(rawDataToServerSubuser(data.data)))
|
||||||
.catch(reject);
|
.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 AuthenticationRouter from '@/routers/AuthenticationRouter';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { SiteSettings } from '@/state/settings';
|
import { SiteSettings } from '@/state/settings';
|
||||||
import { DefaultTheme, ThemeProvider } from 'styled-components';
|
|
||||||
import ProgressBar from '@/components/elements/ProgressBar';
|
import ProgressBar from '@/components/elements/ProgressBar';
|
||||||
import NotFound from '@/components/screens/NotFound';
|
import NotFound from '@/components/screens/NotFound';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
|
||||||
|
|
||||||
interface ExtendedWindow extends Window {
|
interface ExtendedWindow extends Window {
|
||||||
SiteConfiguration?: SiteSettings;
|
SiteConfiguration?: SiteSettings;
|
||||||
|
@ -18,24 +19,16 @@ interface ExtendedWindow extends Window {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
/* eslint-disable camelcase */
|
||||||
root_admin: boolean;
|
root_admin: boolean;
|
||||||
use_totp: boolean;
|
use_totp: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
created_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 App = () => {
|
||||||
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
|
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
|
||||||
if (PterodactylUser && !store.getState().user.data) {
|
if (PterodactylUser && !store.getState().user.data) {
|
||||||
|
@ -56,11 +49,12 @@ const App = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<>
|
||||||
|
<GlobalStylesheet/>
|
||||||
<StoreProvider store={store}>
|
<StoreProvider store={store}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ProgressBar/>
|
<ProgressBar/>
|
||||||
<div className={'mx-auto w-auto'}>
|
<div css={tw`mx-auto w-auto`}>
|
||||||
<BrowserRouter basename={'/'} key={'root-router'}>
|
<BrowserRouter basename={'/'} key={'root-router'}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/server/:id" component={ServerRouter}/>
|
<Route path="/server/:id" component={ServerRouter}/>
|
||||||
|
@ -72,7 +66,7 @@ const App = () => {
|
||||||
</div>
|
</div>
|
||||||
</Provider>
|
</Provider>
|
||||||
</StoreProvider>
|
</StoreProvider>
|
||||||
</ThemeProvider>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,25 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MessageBox from '@/components/MessageBox';
|
import MessageBox from '@/components/MessageBox';
|
||||||
import { State, useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
byKey?: string;
|
byKey?: string;
|
||||||
spacerClass?: string;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default ({ className, spacerClass, byKey }: Props) => {
|
const FlashMessageRender = ({ byKey, className }: Props) => {
|
||||||
const flashes = useStoreState((state: State<ApplicationStore>) => state.flashes.items);
|
const flashes = useStoreState(state => state.flashes.items.filter(
|
||||||
|
flash => byKey ? flash.key === byKey : true,
|
||||||
let filtered = flashes;
|
));
|
||||||
if (byKey) {
|
|
||||||
filtered = flashes.filter(flash => flash.key === byKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
flashes.length ?
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{
|
{
|
||||||
filtered.map((flash, index) => (
|
flashes.map((flash, index) => (
|
||||||
<React.Fragment key={flash.id || flash.type + flash.message}>
|
<React.Fragment key={flash.id || flash.type + flash.message}>
|
||||||
{index > 0 && <div className={spacerClass || 'mt-2'}></div>}
|
{index > 0 && <div css={tw`mt-2`}></div>}
|
||||||
<MessageBox type={flash.type} title={flash.title}>
|
<MessageBox type={flash.type} title={flash.title}>
|
||||||
{flash.message}
|
{flash.message}
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
|
@ -34,5 +27,9 @@ export default ({ className, spacerClass, byKey }: Props) => {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
:
|
||||||
|
null
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default FlashMessageRender;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import tw, { TwStyle } from 'twin.macro';
|
||||||
|
import styled from 'styled-components/macro';
|
||||||
|
|
||||||
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
|
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
|
||||||
|
|
||||||
|
@ -8,11 +10,60 @@ interface Props {
|
||||||
type?: FlashMessageType;
|
type?: FlashMessageType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ title, children, type }: Props) => (
|
const styling = (type?: FlashMessageType): TwStyle | string => {
|
||||||
<div className={`lg:inline-flex alert ${type}`} role={'alert'}>
|
switch (type) {
|
||||||
{title && <span className={'title'}>{title}</span>}
|
case 'error':
|
||||||
<span className={'message'}>
|
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}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Container>
|
||||||
);
|
);
|
||||||
|
MessageBox.displayName = 'MessageBox';
|
||||||
|
|
||||||
|
export default MessageBox;
|
||||||
|
|
|
@ -1,51 +1,77 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link, NavLink } from 'react-router-dom';
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faLayerGroup } from '@fortawesome/free-solid-svg-icons/faLayerGroup';
|
import { faCogs, faLayerGroup, faSignOutAlt, faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
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 { useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
|
|
||||||
import SearchContainer from '@/components/dashboard/search/SearchContainer';
|
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 () => {
|
export default () => {
|
||||||
const user = useStoreState((state: ApplicationStore) => state.user.data!);
|
const user = useStoreState((state: ApplicationStore) => state.user.data!);
|
||||||
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={'navigation'}>
|
<Navigation>
|
||||||
<div className={'mx-auto w-full flex items-center'} style={{ maxWidth: '1200px', height: '3.5rem' }}>
|
<div css={tw`mx-auto w-full flex items-center`} style={{ maxWidth: '1200px', height: '3.5rem' }}>
|
||||||
<div id={'logo'}>
|
<div id={'logo'}>
|
||||||
<Link to={'/'}>
|
<Link to={'/'}>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={'right-navigation'}>
|
<RightNavigation>
|
||||||
<SearchContainer/>
|
<SearchContainer/>
|
||||||
<NavLink to={'/'} exact={true}>
|
<NavLink to={'/'} exact>
|
||||||
<FontAwesomeIcon icon={faLayerGroup}/>
|
<FontAwesomeIcon icon={faLayerGroup}/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink to={'/account'}>
|
<NavLink to={'/account'}>
|
||||||
<FontAwesomeIcon icon={faUserCircle}/>
|
<FontAwesomeIcon icon={faUserCircle}/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{user.rootAdmin &&
|
{user.rootAdmin &&
|
||||||
<a href={'/admin'} target={'_blank'}>
|
<a href={'/admin'} target={'_blank'} rel={'noreferrer'}>
|
||||||
<FontAwesomeIcon icon={faCogs}/>
|
<FontAwesomeIcon icon={faCogs}/>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
{process.env.NODE_ENV !== 'production' &&
|
|
||||||
<NavLink to={'/design'}>
|
|
||||||
<FontAwesomeIcon icon={faSwatchbook}/>
|
|
||||||
</NavLink>
|
|
||||||
}
|
|
||||||
<a href={'/auth/logout'}>
|
<a href={'/auth/logout'}>
|
||||||
<FontAwesomeIcon icon={faSignOutAlt}/>
|
<FontAwesomeIcon icon={faSignOutAlt}/>
|
||||||
</a>
|
</a>
|
||||||
|
</RightNavigation>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Navigation>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 Field from '@/components/elements/Field';
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -43,33 +45,30 @@ export default () => {
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<LoginFormContainer
|
<LoginFormContainer
|
||||||
title={'Request Password Reset'}
|
title={'Request Password Reset'}
|
||||||
className={'w-full flex'}
|
css={tw`w-full flex`}
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
light={true}
|
light
|
||||||
label={'Email'}
|
label={'Email'}
|
||||||
description={'Enter your account email address to receive instructions on resetting your password.'}
|
description={'Enter your account email address to receive instructions on resetting your password.'}
|
||||||
name={'email'}
|
name={'email'}
|
||||||
type={'email'}
|
type={'email'}
|
||||||
/>
|
/>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button
|
<Button
|
||||||
type={'submit'}
|
type={'submit'}
|
||||||
className={'btn btn-primary btn-jumbo flex justify-center'}
|
size={'xlarge'}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ?
|
Send Email
|
||||||
<div className={'spinner-circle spinner-sm spinner-white'}></div>
|
</Button>
|
||||||
:
|
|
||||||
'Send Email'
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6 text-center'}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
type={'button'}
|
type={'button'}
|
||||||
to={'/auth/login'}
|
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
|
Return to Login
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -5,19 +5,19 @@ import { httpErrorToHuman } from '@/api/http';
|
||||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||||
import { ActionCreator } from 'easy-peasy';
|
import { ActionCreator } from 'easy-peasy';
|
||||||
import { StaticContext } from 'react-router';
|
import { StaticContext } from 'react-router';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
|
||||||
import { useFormikContext, withFormik } from 'formik';
|
import { useFormikContext, withFormik } from 'formik';
|
||||||
import { object, string } from 'yup';
|
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import { FlashStore } from '@/state/flashes';
|
import { FlashStore } from '@/state/flashes';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
code: string;
|
code: string;
|
||||||
recoveryCode: '',
|
recoveryCode: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
|
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }>
|
||||||
|
|
||||||
type Props = OwnProps & {
|
type Props = OwnProps & {
|
||||||
addError: ActionCreator<FlashStore['addError']['payload']>;
|
addError: ActionCreator<FlashStore['addError']['payload']>;
|
||||||
|
@ -29,13 +29,10 @@ const LoginCheckpointContainer = () => {
|
||||||
const [ isMissingDevice, setIsMissingDevice ] = useState(false);
|
const [ isMissingDevice, setIsMissingDevice ] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginFormContainer
|
<LoginFormContainer title={'Device Checkpoint'} css={tw`w-full flex`}>
|
||||||
title={'Device Checkpoint'}
|
<div css={tw`mt-6`}>
|
||||||
className={'w-full flex'}
|
|
||||||
>
|
|
||||||
<div className={'mt-6'}>
|
|
||||||
<Field
|
<Field
|
||||||
light={true}
|
light
|
||||||
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
||||||
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
|
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
|
||||||
description={
|
description={
|
||||||
|
@ -44,38 +41,35 @@ const LoginCheckpointContainer = () => {
|
||||||
: 'Enter the two-factor token generated by your device.'
|
: 'Enter the two-factor token generated by your device.'
|
||||||
}
|
}
|
||||||
type={isMissingDevice ? 'text' : 'number'}
|
type={isMissingDevice ? 'text' : 'number'}
|
||||||
autoFocus={true}
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button
|
<Button
|
||||||
|
size={'xlarge'}
|
||||||
type={'submit'}
|
type={'submit'}
|
||||||
className={'btn btn-primary btn-jumbo'}
|
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ?
|
Continue
|
||||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
</Button>
|
||||||
:
|
|
||||||
'Continue'
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6 text-center'}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<span
|
<span
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFieldValue('code', '');
|
setFieldValue('code', '');
|
||||||
setFieldValue('recoveryCode', '');
|
setFieldValue('recoveryCode', '');
|
||||||
setIsMissingDevice(s => !s);
|
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'}
|
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6 text-center'}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/login'}
|
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
|
Return to Login
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -10,7 +10,8 @@ import Field from '@/components/elements/Field';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { FlashMessage } from '@/state/flashes';
|
import { FlashMessage } from '@/state/flashes';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
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 & {
|
type OwnProps = RouteComponentProps & {
|
||||||
clearFlashes: ActionCreator<void>;
|
clearFlashes: ActionCreator<void>;
|
||||||
|
@ -34,38 +35,27 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{ref.current && ref.current.render()}
|
{ref.current && ref.current.render()}
|
||||||
<LoginFormContainer
|
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}>
|
||||||
title={'Login to Continue'}
|
|
||||||
className={'w-full flex'}
|
|
||||||
onSubmit={submit}
|
|
||||||
>
|
|
||||||
<label htmlFor={'username'}>Username or Email</label>
|
|
||||||
<Field
|
<Field
|
||||||
type={'text'}
|
type={'text'}
|
||||||
|
label={'Username or Email'}
|
||||||
id={'username'}
|
id={'username'}
|
||||||
name={'username'}
|
name={'username'}
|
||||||
className={'input'}
|
light
|
||||||
/>
|
/>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<label htmlFor={'password'}>Password</label>
|
|
||||||
<Field
|
<Field
|
||||||
type={'password'}
|
type={'password'}
|
||||||
|
label={'Password'}
|
||||||
id={'password'}
|
id={'password'}
|
||||||
name={'password'}
|
name={'password'}
|
||||||
className={'input'}
|
light
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button
|
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
|
||||||
type={'submit'}
|
Login
|
||||||
className={'btn btn-primary btn-jumbo'}
|
</Button>
|
||||||
>
|
|
||||||
{isSubmitting ?
|
|
||||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
|
||||||
:
|
|
||||||
'Login'
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{recaptchaEnabled &&
|
{recaptchaEnabled &&
|
||||||
<ReCAPTCHA
|
<ReCAPTCHA
|
||||||
|
@ -80,10 +70,10 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||||
onExpired={() => setFieldValue('recaptchaData', null)}
|
onExpired={() => setFieldValue('recaptchaData', null)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div className={'mt-6 text-center'}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/password'}
|
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?
|
Forgot password?
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -96,7 +86,7 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||||
const EnhancedForm = withFormik<OwnProps, LoginData>({
|
const EnhancedForm = withFormik<OwnProps, LoginData>({
|
||||||
displayName: 'LoginContainerForm',
|
displayName: 'LoginContainerForm',
|
||||||
|
|
||||||
mapPropsToValues: (props) => ({
|
mapPropsToValues: () => ({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
recaptchaData: null,
|
recaptchaData: null,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { Form } from 'formik';
|
import { Form } from 'formik';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components/macro';
|
||||||
import { breakpoint } from 'styled-components-breakpoint';
|
import { breakpoint } from '@/theme';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -29,27 +30,29 @@ const Container = styled.div`
|
||||||
|
|
||||||
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
||||||
<Container>
|
<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}
|
{title}
|
||||||
</h2>}
|
</h2>
|
||||||
<FlashMessageRender className={'mb-2 px-1'}/>
|
}
|
||||||
|
<FlashMessageRender css={tw`mb-2 px-1`}/>
|
||||||
<Form {...props} ref={ref}>
|
<Form {...props} ref={ref}>
|
||||||
<div className={'md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1'}>
|
<div css={tw`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'}>
|
<div css={tw`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'}/>
|
<img src={'/assets/svgs/pterodactyl.svg'} css={tw`block w-48 md:w-64 mx-auto`}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex-1'}>
|
<div css={tw`flex-1`}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</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
|
© 2015 - 2020
|
||||||
<a
|
<a
|
||||||
rel={'noopener nofollow'}
|
rel={'noopener nofollow noreferrer'}
|
||||||
href={'https://pterodactyl.io'}
|
href={'https://pterodactyl.io'}
|
||||||
target={'_blank'}
|
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
|
Pterodactyl Software
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -7,19 +7,19 @@ import { httpErrorToHuman } from '@/api/http';
|
||||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
import { Actions, useStoreActions } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
import { object, ref, string } from 'yup';
|
import { object, ref, string } from 'yup';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
|
import Input from '@/components/elements/Input';
|
||||||
type Props = Readonly<RouteComponentProps<{ token: string }> & {}>;
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
password: string;
|
password: string;
|
||||||
passwordConfirmation: string;
|
passwordConfirmation: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ match, history, location }: Props) => {
|
export default ({ match, location }: RouteComponentProps<{ token: string }>) => {
|
||||||
const [ email, setEmail ] = useState('');
|
const [ email, setEmail ] = useState('');
|
||||||
|
|
||||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
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.'),
|
.min(8, 'Your new password should be at least 8 characters in length.'),
|
||||||
passwordConfirmation: string()
|
passwordConfirmation: string()
|
||||||
.required('Your new password does not match.')
|
.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 }) => (
|
{({ isSubmitting }) => (
|
||||||
<LoginFormContainer
|
<LoginFormContainer
|
||||||
title={'Reset Password'}
|
title={'Reset Password'}
|
||||||
className={'w-full flex'}
|
css={tw`w-full flex`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label>Email</label>
|
<label>Email</label>
|
||||||
<input className={'input'} value={email} disabled={true}/>
|
<Input value={email} isLight disabled/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field
|
||||||
light={true}
|
light
|
||||||
label={'New Password'}
|
label={'New Password'}
|
||||||
name={'password'}
|
name={'password'}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
description={'Passwords must be at least 8 characters in length.'}
|
description={'Passwords must be at least 8 characters in length.'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field
|
||||||
light={true}
|
light
|
||||||
label={'Confirm New Password'}
|
label={'Confirm New Password'}
|
||||||
name={'passwordConfirmation'}
|
name={'passwordConfirmation'}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button
|
<Button
|
||||||
|
size={'xlarge'}
|
||||||
type={'submit'}
|
type={'submit'}
|
||||||
className={'btn btn-primary btn-jumbo'}
|
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ?
|
Reset Password
|
||||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
</Button>
|
||||||
:
|
|
||||||
'Reset Password'
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6 text-center'}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/login'}
|
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
|
Return to Login
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -4,16 +4,17 @@ import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
|
||||||
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
|
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faKey } from '@fortawesome/free-solid-svg-icons/faKey';
|
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
|
||||||
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
||||||
import deleteApiKey from '@/api/account/deleteApiKey';
|
import deleteApiKey from '@/api/account/deleteApiKey';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
import { Actions, useStoreActions } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import format from 'date-fns/format';
|
import { format } from 'date-fns';
|
||||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
|
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
|
||||||
|
@ -48,18 +49,18 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
<FlashMessageRender byKey={'account'} className={'mb-4'}/>
|
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
|
||||||
<div className={'flex'}>
|
<div css={tw`flex`}>
|
||||||
<ContentBox title={'Create API Key'} className={'flex-1'}>
|
<ContentBox title={'Create API Key'} css={tw`flex-1`}>
|
||||||
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
|
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
<ContentBox title={'API Keys'} className={'ml-10 flex-1'}>
|
<ContentBox title={'API Keys'} css={tw`ml-10 flex-1`}>
|
||||||
<SpinnerOverlay visible={loading}/>
|
<SpinnerOverlay visible={loading}/>
|
||||||
{deleteIdentifier &&
|
{deleteIdentifier &&
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
|
visible
|
||||||
title={'Confirm key deletion'}
|
title={'Confirm key deletion'}
|
||||||
buttonText={'Yes, delete key'}
|
buttonText={'Yes, delete key'}
|
||||||
visible={true}
|
|
||||||
onConfirmed={() => {
|
onConfirmed={() => {
|
||||||
doDeletion(deleteIdentifier);
|
doDeletion(deleteIdentifier);
|
||||||
setDeleteIdentifier('');
|
setDeleteIdentifier('');
|
||||||
|
@ -72,38 +73,38 @@ export default () => {
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
keys.length === 0 ?
|
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.'}
|
{loading ? 'Loading...' : 'No API keys exist for this account.'}
|
||||||
</p>
|
</p>
|
||||||
:
|
:
|
||||||
keys.map(key => (
|
keys.map((key, index) => (
|
||||||
<div
|
<GreyRowBox
|
||||||
key={key.identifier}
|
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'}/>
|
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`}/>
|
||||||
<div className={'ml-4 flex-1'}>
|
<div css={tw`ml-4 flex-1`}>
|
||||||
<p className={'text-sm'}>{key.description}</p>
|
<p css={tw`text-sm`}>{key.description}</p>
|
||||||
<p className={'text-2xs text-neutral-300 uppercase'}>
|
<p css={tw`text-2xs text-neutral-300 uppercase`}>
|
||||||
Last
|
Last used:
|
||||||
used: {key.lastUsedAt ? format(key.lastUsedAt, 'MMM Do, YYYY HH:mm') : 'Never'}
|
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className={'text-sm ml-4'}>
|
<p css={tw`text-sm ml-4`}>
|
||||||
<code className={'font-mono py-1 px-2 bg-neutral-900 rounded'}>
|
<code css={tw`font-mono py-1 px-2 bg-neutral-900 rounded`}>
|
||||||
{key.identifier}
|
{key.identifier}
|
||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
className={'ml-4 p-2 text-sm'}
|
css={tw`ml-4 p-2 text-sm`}
|
||||||
onClick={() => setDeleteIdentifier(key.identifier)}
|
onClick={() => setDeleteIdentifier(key.identifier)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faTrashAlt}
|
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>
|
</button>
|
||||||
</div>
|
</GreyRowBox>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
|
|
|
@ -3,9 +3,10 @@ import ContentBox from '@/components/elements/ContentBox';
|
||||||
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
||||||
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
||||||
import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFactorForm';
|
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 PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import { breakpoint } from '@/theme';
|
||||||
|
import styled from 'styled-components/macro';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
${tw`flex flex-wrap my-10`};
|
${tw`flex flex-wrap my-10`};
|
||||||
|
@ -31,13 +32,13 @@ export default () => {
|
||||||
<UpdatePasswordForm/>
|
<UpdatePasswordForm/>
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
<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'}
|
title={'Update Email Address'}
|
||||||
showFlashes={'account:email'}
|
showFlashes={'account:email'}
|
||||||
>
|
>
|
||||||
<UpdateEmailAddressForm/>
|
<UpdateEmailAddressForm/>
|
||||||
</ContentBox>
|
</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/>
|
<ConfigureTwoFactorForm/>
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
import { usePersistedState } from '@/plugins/usePersistedState';
|
import { usePersistedState } from '@/plugins/usePersistedState';
|
||||||
import Switch from '@/components/elements/Switch';
|
import Switch from '@/components/elements/Switch';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { addError, clearFlashes } = useFlash();
|
const { addError, clearFlashes } = useFlash();
|
||||||
|
@ -37,10 +38,10 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
<FlashMessageRender className={'mb-4'}/>
|
<FlashMessageRender css={tw`mb-4`}/>
|
||||||
{rootAdmin &&
|
{rootAdmin &&
|
||||||
<div className={'mb-2 flex justify-end items-center'}>
|
<div css={tw`mb-2 flex justify-end items-center`}>
|
||||||
<p className={'uppercase text-xs text-neutral-400 mr-2'}>
|
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
|
||||||
{showAdmin ? 'Showing all servers' : 'Showing your servers'}
|
{showAdmin ? 'Showing all servers' : 'Showing your servers'}
|
||||||
</p>
|
</p>
|
||||||
<Switch
|
<Switch
|
||||||
|
@ -51,14 +52,16 @@ export default () => {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{loading ?
|
{loading ?
|
||||||
<Spinner centered={true} size={'large'}/>
|
<Spinner centered size={'large'}/>
|
||||||
:
|
:
|
||||||
servers.length > 0 ?
|
servers.length > 0 ?
|
||||||
servers.map(server => (
|
servers.map((server, index) => (
|
||||||
<ServerRow key={server.uuid} server={server} className={'mt-2'}/>
|
<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.
|
There are no servers associated with your account.
|
||||||
</p>
|
</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 React, { useEffect, useRef, useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
|
import { faServer, faEthernet, faMicrochip, faMemory, faHdd } from '@fortawesome/free-solid-svg-icons';
|
||||||
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 { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Server } from '@/api/server/getServer';
|
import { Server } from '@/api/server/getServer';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
|
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
|
||||||
import { bytesToHuman } from '@/helpers';
|
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
|
// Determines if the current value is in an alarm threshold so we can show it in red rather
|
||||||
// than the more faded default style.
|
// than the more faded default style.
|
||||||
|
@ -20,7 +17,7 @@ const isAlarmState = (current: number, limit: number): boolean => {
|
||||||
return current / limitInBytes >= 0.90;
|
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 interval = useRef<number>(null);
|
||||||
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
||||||
const [ statsError, setStatsError ] = useState(false);
|
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.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
|
||||||
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
|
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 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 memorylimit = server.limits.memory !== 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : 'Unlimited';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/server/${server.id}`} className={`grey-row-box cursor-pointer ${className}`}>
|
<GreyRowBox as={Link} to={`/server/${server.id}`}>
|
||||||
<div className={'icon'}>
|
<div className={'icon'}>
|
||||||
<FontAwesomeIcon icon={faServer}/>
|
<FontAwesomeIcon icon={faServer}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex-1 ml-4'}>
|
<div css={tw`flex-1 ml-4`}>
|
||||||
<p className={'text-lg'}>{server.name}</p>
|
<p css={tw`text-lg`}>{server.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-1/4 overflow-hidden'}>
|
<div css={tw`w-1/4 overflow-hidden`}>
|
||||||
<div className={'flex ml-4'}>
|
<div css={tw`flex ml-4`}>
|
||||||
<FontAwesomeIcon icon={faEthernet} className={'text-neutral-500'}/>
|
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
|
||||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
<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>
|
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-1/3 flex items-baseline relative'}>
|
<div css={tw`w-1/3 flex items-baseline relative`}>
|
||||||
{!stats ?
|
{!stats ?
|
||||||
!statsError ?
|
!statsError ?
|
||||||
<SpinnerOverlay size={'tiny'} visible={true} backgroundOpacity={0.25}/>
|
<SpinnerOverlay size={'small'} visible backgroundOpacity={0.25}/>
|
||||||
:
|
:
|
||||||
server.isInstalling ?
|
server.isInstalling ?
|
||||||
<div className={'flex-1 text-center'}>
|
<div css={tw`flex-1 text-center`}>
|
||||||
<span className={'bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs'}>
|
<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
|
||||||
Installing
|
Installing
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div className={'flex-1 text-center'}>
|
<div css={tw`flex-1 text-center`}>
|
||||||
<span className={'bg-red-500 rounded px-2 py-1 text-red-100 text-xs'}>
|
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
|
||||||
{server.isSuspended ? 'Suspended' : 'Connection Error'}
|
{server.isSuspended ? 'Suspended' : 'Connection Error'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className={'flex-1 flex ml-4 justify-center'}>
|
<div css={tw`flex-1 flex ml-4 justify-center`}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faMicrochip}
|
icon={faMicrochip}
|
||||||
className={classNames({
|
css={[
|
||||||
'text-neutral-500': !alarms.cpu,
|
!alarms.cpu && tw`text-neutral-500`,
|
||||||
'text-red-400': alarms.cpu,
|
alarms.cpu && tw`text-red-400`,
|
||||||
})}
|
]}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className={classNames('text-sm ml-2', {
|
css={[
|
||||||
'text-neutral-400': !alarms.cpu,
|
tw`text-sm ml-2`,
|
||||||
'text-white': alarms.cpu,
|
!alarms.cpu && tw`text-neutral-400`,
|
||||||
})}
|
alarms.cpu && tw`text-white`,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{stats.cpuUsagePercent} %
|
{stats.cpuUsagePercent} %
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex-1 ml-4'}>
|
<div css={tw`flex-1 ml-4`}>
|
||||||
<div className={'flex justify-center'}>
|
<div css={tw`flex justify-center`}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faMemory}
|
icon={faMemory}
|
||||||
className={classNames({
|
css={[
|
||||||
'text-neutral-500': !alarms.memory,
|
!alarms.memory && tw`text-neutral-500`,
|
||||||
'text-red-400': alarms.memory,
|
alarms.memory && tw`text-red-400`,
|
||||||
})}
|
]}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className={classNames('text-sm ml-2', {
|
css={[
|
||||||
'text-neutral-400': !alarms.memory,
|
tw`text-sm ml-2`,
|
||||||
'text-white': alarms.memory,
|
!alarms.memory && tw`text-neutral-400`,
|
||||||
})}
|
alarms.memory && tw`text-white`,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{bytesToHuman(stats.memoryUsageInBytes)}
|
{bytesToHuman(stats.memoryUsageInBytes)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
<div className={'flex-1 ml-4'}>
|
<div css={tw`flex-1 ml-4`}>
|
||||||
<div className={'flex justify-center'}>
|
<div css={tw`flex justify-center`}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faHdd}
|
icon={faHdd}
|
||||||
className={classNames({
|
css={[
|
||||||
'text-neutral-500': !alarms.disk,
|
!alarms.disk && tw`text-neutral-500`,
|
||||||
'text-red-400': alarms.disk,
|
alarms.disk && tw`text-red-400`,
|
||||||
})}
|
]}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className={classNames('text-sm ml-2', {
|
css={[
|
||||||
'text-neutral-400': !alarms.disk,
|
tw`text-sm ml-2`,
|
||||||
'text-white': alarms.disk,
|
!alarms.disk && tw`text-neutral-400`,
|
||||||
})}
|
alarms.disk && tw`text-white`,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{bytesToHuman(stats.diskUsageInBytes)}
|
{bytesToHuman(stats.diskUsageInBytes)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</GreyRowBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
|
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
|
||||||
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
|
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const user = useStoreState((state: ApplicationStore) => state.user.data!);
|
const user = useStoreState((state: ApplicationStore) => state.user.data!);
|
||||||
|
@ -12,43 +14,45 @@ export default () => {
|
||||||
<div>
|
<div>
|
||||||
{visible &&
|
{visible &&
|
||||||
<DisableTwoFactorModal
|
<DisableTwoFactorModal
|
||||||
appear={true}
|
appear
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onDismissed={() => setVisible(false)}
|
onDismissed={() => setVisible(false)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<p className={'text-sm'}>
|
<p css={tw`text-sm`}>
|
||||||
Two-factor authentication is currently enabled on your account.
|
Two-factor authentication is currently enabled on your account.
|
||||||
</p>
|
</p>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button
|
<Button
|
||||||
|
color={'red'}
|
||||||
|
isSecondary
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setVisible(true)}
|
||||||
className={'btn btn-red btn-secondary btn-sm'}
|
|
||||||
>
|
>
|
||||||
Disable
|
Disable
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div>
|
<div>
|
||||||
{visible &&
|
{visible &&
|
||||||
<SetupTwoFactorModal
|
<SetupTwoFactorModal
|
||||||
appear={true}
|
appear
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onDismissed={() => setVisible(false)}
|
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
|
You do not currently have two-factor authentication enabled on your account. Click
|
||||||
the button below to begin configuring it.
|
the button below to begin configuring it.
|
||||||
</p>
|
</p>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button
|
<Button
|
||||||
|
color={'green'}
|
||||||
|
isSecondary
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setVisible(true)}
|
||||||
className={'btn btn-green btn-secondary btn-sm'}
|
|
||||||
>
|
>
|
||||||
Begin Setup
|
Begin Setup
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
;
|
;
|
||||||
|
|
|
@ -9,6 +9,9 @@ import { ApplicationStore } from '@/state';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import { ApiKey } from '@/api/account/getApiKeys';
|
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 {
|
interface Values {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -44,22 +47,21 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||||
closeOnEscape={false}
|
closeOnEscape={false}
|
||||||
closeOnBackground={false}
|
closeOnBackground={false}
|
||||||
>
|
>
|
||||||
<h3 className={'mb-6'}>Your API Key</h3>
|
<h3 css={tw`mb-6`}>Your API Key</h3>
|
||||||
<p className={'text-sm mb-6'}>
|
<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
|
The API key you have requested is shown below. Please store this in a safe location, it will not be
|
||||||
shown again.
|
shown again.
|
||||||
</p>
|
</p>
|
||||||
<pre className={'text-sm bg-neutral-900 rounded py-2 px-4 font-mono'}>
|
<pre css={tw`text-sm bg-neutral-900 rounded py-2 px-4 font-mono`}>
|
||||||
<code className={'font-mono'}>{apiKey}</code>
|
<code css={tw`font-mono`}>{apiKey}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<div className={'flex justify-end mt-6'}>
|
<div css={tw`flex justify-end mt-6`}>
|
||||||
<button
|
<Button
|
||||||
type={'button'}
|
type={'button'}
|
||||||
className={'btn btn-secondary btn-sm'}
|
|
||||||
onClick={() => setApiKey('')}
|
onClick={() => setApiKey('')}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Formik
|
<Formik
|
||||||
|
@ -80,25 +82,19 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||||
label={'Description'}
|
label={'Description'}
|
||||||
name={'description'}
|
name={'description'}
|
||||||
description={'A description of this API key.'}
|
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>
|
||||||
<FormikFieldWrapper
|
<FormikFieldWrapper
|
||||||
label={'Allowed IPs'}
|
label={'Allowed IPs'}
|
||||||
name={'allowedIps'}
|
name={'allowedIps'}
|
||||||
description={'Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line.'}
|
description={'Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line.'}
|
||||||
>
|
>
|
||||||
<Field
|
<Field as={Textarea} name={'allowedIps'} css={tw`h-32`}/>
|
||||||
as={'textarea'}
|
|
||||||
name={'allowedIps'}
|
|
||||||
className={'input-dark h-32'}
|
|
||||||
/>
|
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
<div className={'flex justify-end mt-6'}>
|
<div css={tw`flex justify-end mt-6`}>
|
||||||
<button className={'btn btn-primary btn-sm'}>
|
<Button>Create</Button>
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { Actions, useStoreActions } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
|
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
password: string;
|
password: string;
|
||||||
|
@ -45,19 +47,19 @@ export default ({ ...props }: RequiredModalProps) => {
|
||||||
{({ isSubmitting, isValid }) => (
|
{({ isSubmitting, isValid }) => (
|
||||||
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
|
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
|
||||||
<Form className={'mb-0'}>
|
<Form className={'mb-0'}>
|
||||||
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
|
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'}/>
|
||||||
<Field
|
<Field
|
||||||
id={'password'}
|
id={'password'}
|
||||||
name={'password'}
|
name={'password'}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
label={'Current Password'}
|
label={'Current Password'}
|
||||||
description={'In order to disable two-factor authentication you will need to provide your account 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'}>
|
<div css={tw`mt-6 text-right`}>
|
||||||
<button className={'btn btn-red btn-sm'} disabled={!isValid}>
|
<Button disabled={!isValid}>
|
||||||
Disable Two-Factor
|
Disable Two-Factor
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { ApplicationStore } from '@/state';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -64,7 +66,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
||||||
.matches(/^(\d){6}$/, 'Authenticator code must be 6 digits.'),
|
.matches(/^(\d){6}$/, 'Authenticator code must be 6 digits.'),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{({ isSubmitting, isValid }) => (
|
{({ isSubmitting }) => (
|
||||||
<Modal
|
<Modal
|
||||||
{...props}
|
{...props}
|
||||||
onDismissed={dismiss}
|
onDismissed={dismiss}
|
||||||
|
@ -75,47 +77,47 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
||||||
>
|
>
|
||||||
{recoveryTokens.length > 0 ?
|
{recoveryTokens.length > 0 ?
|
||||||
<>
|
<>
|
||||||
<h2 className={'mb-4'}>Two-factor authentication enabled</h2>
|
<h2 css={tw`text-2xl mb-4`}>Two-factor authentication enabled</h2>
|
||||||
<p className={'text-neutral-300'}>
|
<p css={tw`text-neutral-300`}>
|
||||||
Two-factor authentication has been enabled on your account. Should you loose access to
|
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.
|
account.
|
||||||
</p>
|
</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
|
<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.
|
by storing them in a secure repository such as a password manager.
|
||||||
</p>
|
</p>
|
||||||
<pre className={'mt-4 rounded font-mono bg-neutral-900 p-4'}>
|
<pre css={tw`text-sm mt-4 rounded font-mono bg-neutral-900 p-4`}>
|
||||||
{recoveryTokens.map(token => <code key={token} className={'block mb-1'}>{token}</code>)}
|
{recoveryTokens.map(token => <code key={token} css={tw`block mb-1`}>{token}</code>)}
|
||||||
</pre>
|
</pre>
|
||||||
<div className={'text-right'}>
|
<div css={tw`text-right`}>
|
||||||
<button className={'mt-6 btn btn-lg btn-primary'} onClick={dismiss}>
|
<Button css={tw`mt-6`} size={'large'} onClick={dismiss}>
|
||||||
Close
|
Close
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
:
|
:
|
||||||
<Form className={'mb-0'}>
|
<Form css={tw`mb-0`}>
|
||||||
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
|
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'}/>
|
||||||
<div className={'flex flex-wrap'}>
|
<div css={tw`flex flex-wrap`}>
|
||||||
<div className={'w-full md:flex-1'}>
|
<div css={tw`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'}>
|
<div css={tw`w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto`}>
|
||||||
{!token || !token.length ?
|
{!token || !token.length ?
|
||||||
<img
|
<img
|
||||||
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
|
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
|
||||||
className={'w-64 h-64 rounded'}
|
css={tw`w-64 h-64 rounded`}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<img
|
<img
|
||||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
|
||||||
onLoad={() => setLoading(false)}
|
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>
|
</div>
|
||||||
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
|
<div css={tw`w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col`}>
|
||||||
<div className={'flex-1'}>
|
<div css={tw`flex-1`}>
|
||||||
<Field
|
<Field
|
||||||
id={'code'}
|
id={'code'}
|
||||||
name={'code'}
|
name={'code'}
|
||||||
|
@ -125,10 +127,10 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
||||||
autoFocus={!loading}
|
autoFocus={!loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6 md:mt-0 text-right'}>
|
<div css={tw`mt-6 md:mt-0 text-right`}>
|
||||||
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
|
<Button>
|
||||||
Setup
|
Setup
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,8 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -54,14 +56,14 @@ export default () => {
|
||||||
({ isSubmitting, isValid }) => (
|
({ isSubmitting, isValid }) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
|
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
|
||||||
<Form className={'m-0'}>
|
<Form css={tw`m-0`}>
|
||||||
<Field
|
<Field
|
||||||
id={'current_email'}
|
id={'current_email'}
|
||||||
type={'email'}
|
type={'email'}
|
||||||
name={'email'}
|
name={'email'}
|
||||||
label={'Email'}
|
label={'Email'}
|
||||||
/>
|
/>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field
|
||||||
id={'confirm_password'}
|
id={'confirm_password'}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
|
@ -69,10 +71,10 @@ export default () => {
|
||||||
label={'Confirm Password'}
|
label={'Confirm Password'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button className={'btn btn-sm btn-primary'} disabled={isSubmitting || !isValid}>
|
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
||||||
Update Email
|
Update Email
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -7,6 +7,8 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import updateAccountPassword from '@/api/account/updateAccountPassword';
|
import updateAccountPassword from '@/api/account/updateAccountPassword';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
current: string;
|
current: string;
|
||||||
|
@ -30,7 +32,7 @@ export default () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = (values: Values, { resetForm, setSubmitting }: FormikHelpers<Values>) => {
|
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
clearFlashes('account:password');
|
clearFlashes('account:password');
|
||||||
updateAccountPassword({ ...values })
|
updateAccountPassword({ ...values })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -57,14 +59,14 @@ export default () => {
|
||||||
({ isSubmitting, isValid }) => (
|
({ isSubmitting, isValid }) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
|
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
|
||||||
<Form className={'m-0'}>
|
<Form css={tw`m-0`}>
|
||||||
<Field
|
<Field
|
||||||
id={'current_password'}
|
id={'current_password'}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
name={'current'}
|
name={'current'}
|
||||||
label={'Current Password'}
|
label={'Current Password'}
|
||||||
/>
|
/>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field
|
||||||
id={'new_password'}
|
id={'new_password'}
|
||||||
type={'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.'}
|
description={'Your new password should be at least 8 characters in length and unique to this website.'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field
|
||||||
id={'confirm_password'}
|
id={'confirm_password'}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
|
@ -81,10 +83,10 @@ export default () => {
|
||||||
label={'Confirm New Password'}
|
label={'Confirm New Password'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button className={'btn btn-primary btn-sm'} disabled={isSubmitting || !isValid}>
|
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
||||||
Update Password
|
Update Password
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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 useEventListener from '@/plugins/useEventListener';
|
||||||
import SearchModal from '@/components/dashboard/search/SearchModal';
|
import SearchModal from '@/components/dashboard/search/SearchModal';
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export default () => {
|
||||||
<>
|
<>
|
||||||
{visible &&
|
{visible &&
|
||||||
<SearchModal
|
<SearchModal
|
||||||
appear={true}
|
appear
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onDismissed={() => setVisible(false)}
|
onDismissed={() => setVisible(false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||||
import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
import { debounce } from 'lodash-es';
|
import debounce from 'debounce';
|
||||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||||
import InputSpinner from '@/components/elements/InputSpinner';
|
import InputSpinner from '@/components/elements/InputSpinner';
|
||||||
import getServers from '@/api/getServers';
|
import getServers from '@/api/getServers';
|
||||||
|
@ -11,7 +11,9 @@ import { Server } from '@/api/server/getServer';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { Link } from 'react-router-dom';
|
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;
|
type Props = RequiredModalProps;
|
||||||
|
|
||||||
|
@ -20,8 +22,7 @@ interface Values {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServerResult = styled(Link)`
|
const ServerResult = styled(Link)`
|
||||||
${tw`flex items-center bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline`};
|
${tw`flex items-center bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline transition-all duration-150`};
|
||||||
transition: all 250ms linear;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
${tw`shadow border-cyan-500`};
|
${tw`shadow border-cyan-500`};
|
||||||
|
@ -55,6 +56,7 @@ export default ({ ...props }: Props) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
clearFlashes('search');
|
clearFlashes('search');
|
||||||
|
|
||||||
getServers(term)
|
getServers(term)
|
||||||
.then(servers => setServers(servers.items.filter((_, index) => index < 5)))
|
.then(servers => setServers(servers.items.filter((_, index) => index < 5)))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -93,16 +95,12 @@ export default ({ ...props }: Props) => {
|
||||||
>
|
>
|
||||||
<SearchWatcher/>
|
<SearchWatcher/>
|
||||||
<InputSpinner visible={loading}>
|
<InputSpinner visible={loading}>
|
||||||
<Field
|
<Field as={Input} innerRef={ref} name={'term'}/>
|
||||||
innerRef={ref}
|
|
||||||
name={'term'}
|
|
||||||
className={'input-dark'}
|
|
||||||
/>
|
|
||||||
</InputSpinner>
|
</InputSpinner>
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
</Form>
|
</Form>
|
||||||
{servers.length > 0 &&
|
{servers.length > 0 &&
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
{
|
{
|
||||||
servers.map(server => (
|
servers.map(server => (
|
||||||
<ServerResult
|
<ServerResult
|
||||||
|
@ -111,17 +109,17 @@ export default ({ ...props }: Props) => {
|
||||||
onClick={() => props.onDismissed()}
|
onClick={() => props.onDismissed()}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className={'text-sm'}>{server.name}</p>
|
<p css={tw`text-sm`}>{server.name}</p>
|
||||||
<p className={'mt-1 text-xs text-neutral-400'}>
|
<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>
|
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex-1 text-right'}>
|
<div css={tw`flex-1 text-right`}>
|
||||||
<span className={'text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded'}>
|
<span css={tw`text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded`}>
|
||||||
{server.node}
|
{server.node}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useCallback, useEffect, useState, lazy } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import useRouter from 'use-react-router';
|
|
||||||
import { ServerContext } from '@/state/server';
|
|
||||||
import ace, { Editor } from 'brace';
|
import ace, { Editor } from 'brace';
|
||||||
import getFileContents from '@/api/server/files/getFileContents';
|
import styled from 'styled-components/macro';
|
||||||
import styled from 'styled-components';
|
import tw from 'twin.macro';
|
||||||
|
import Select from '@/components/elements/Select';
|
||||||
|
// @ts-ignore
|
||||||
|
import modes from '@/modes';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
require('brace/ext/modelist');
|
require('brace/ext/modelist');
|
||||||
|
@ -11,7 +12,7 @@ require('ayu-ace/mirage');
|
||||||
|
|
||||||
const EditorContainer = styled.div`
|
const EditorContainer = styled.div`
|
||||||
min-height: 16rem;
|
min-height: 16rem;
|
||||||
height: calc(100vh - 16rem);
|
height: calc(100vh - 20rem);
|
||||||
${tw`relative`};
|
${tw`relative`};
|
||||||
|
|
||||||
#editor {
|
#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}`));
|
Object.keys(modes).forEach(mode => require(`brace/mode/${mode}`));
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -70,7 +42,7 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editor && editor.session.setMode(mode);
|
editor && editor.session.setMode(mode);
|
||||||
}, [editor, mode]);
|
}, [ editor, mode ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editor && editor.session.setValue(initialContent || '');
|
editor && editor.session.setValue(initialContent || '');
|
||||||
|
@ -113,19 +85,18 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten
|
||||||
return (
|
return (
|
||||||
<EditorContainer style={style}>
|
<EditorContainer style={style}>
|
||||||
<div id={'editor'} ref={ref}/>
|
<div id={'editor'} ref={ref}/>
|
||||||
<div className={'absolute pin-r pin-b z-50'}>
|
<div css={tw`absolute right-0 bottom-0 z-50`}>
|
||||||
<div className={'m-3 rounded bg-neutral-900 border border-black'}>
|
<div css={tw`m-3 rounded bg-neutral-900 border border-black`}>
|
||||||
<select
|
<Select
|
||||||
className={'input-dark'}
|
|
||||||
value={mode.split('/').pop()}
|
value={mode.split('/').pop()}
|
||||||
onChange={e => setMode(`ace/mode/${e.currentTarget.value}`)}
|
onChange={e => setMode(`ace/mode/${e.currentTarget.value}`)}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
Object.keys(modes).map(key => (
|
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>
|
||||||
</div>
|
</div>
|
||||||
</EditorContainer>
|
</EditorContainer>
|
||||||
|
|
|
@ -1,20 +1,99 @@
|
||||||
import React from 'react';
|
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) => (
|
const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
||||||
<button
|
${tw`relative inline-block rounded p-2 uppercase tracking-wide text-sm transition-all duration-150 border`};
|
||||||
{...props}
|
|
||||||
className={classNames('btn btn-sm relative', className)}
|
${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 &&
|
{isLoading &&
|
||||||
<div className={'w-full flex absolute justify-center'} style={{ marginLeft: '-0.75rem' }}>
|
<div css={tw`flex absolute justify-center items-center w-full h-full left-0 top-0`}>
|
||||||
<div className={'spinner-circle spinner-white spinner-sm'}/>
|
<Spinner size={'small'}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<span className={isLoading ? 'text-transparent' : undefined}>
|
<span css={isLoading ? tw`text-transparent` : undefined}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</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 React from 'react';
|
||||||
import { Field, FieldProps } from 'formik';
|
import { Field, FieldProps } from 'formik';
|
||||||
|
import Input from '@/components/elements/Input';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
value: 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) => (
|
const Checkbox = ({ name, value, ...props }: Props & InputProps) => (
|
||||||
<Field name={name}>
|
<Field name={name}>
|
||||||
|
@ -20,7 +21,7 @@ const Checkbox = ({ name, value, ...props }: Props & InputProps) => (
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
{...props}
|
{...props}
|
||||||
type={'checkbox'}
|
type={'checkbox'}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -16,15 +18,15 @@ const ConfirmationModal = ({ title, appear, children, visible, buttonText, onCon
|
||||||
showSpinnerOverlay={showSpinnerOverlay}
|
showSpinnerOverlay={showSpinnerOverlay}
|
||||||
onDismissed={() => onDismissed()}
|
onDismissed={() => onDismissed()}
|
||||||
>
|
>
|
||||||
<h3 className={'mb-6'}>{title}</h3>
|
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
|
||||||
<p className={'text-sm'}>{children}</p>
|
<p css={tw`text-sm`}>{children}</p>
|
||||||
<div className={'flex items-center justify-end mt-8'}>
|
<div css={tw`flex items-center justify-end mt-8`}>
|
||||||
<button className={'btn btn-secondary btn-sm'} onClick={() => onDismissed()}>
|
<Button isSecondary onClick={() => onDismissed()}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
<button className={'btn btn-red btn-sm ml-4'} onClick={() => onConfirmed()}>
|
<Button color={'red'} css={tw`ml-4`} onClick={() => onConfirmed()}>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -12,16 +12,19 @@ type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElemen
|
||||||
|
|
||||||
const ContentBox = ({ title, borderColor, showFlashes, showLoadingOverlay, children, ...props }: Props) => (
|
const ContentBox = ({ title, borderColor, showFlashes, showLoadingOverlay, children, ...props }: Props) => (
|
||||||
<div {...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 &&
|
{showFlashes &&
|
||||||
<FlashMessageRender
|
<FlashMessageRender
|
||||||
byKey={typeof showFlashes === 'string' ? showFlashes : undefined}
|
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, {
|
<div
|
||||||
'border-t-4': !!borderColor,
|
css={[
|
||||||
})}>
|
tw`bg-neutral-700 p-4 rounded shadow-lg relative`,
|
||||||
|
!!borderColor && tw`border-t-4`,
|
||||||
|
]}
|
||||||
|
>
|
||||||
<SpinnerOverlay visible={showLoadingOverlay || false}/>
|
<SpinnerOverlay visible={showLoadingOverlay || false}/>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components/macro';
|
||||||
import { breakpoint } from 'styled-components-breakpoint';
|
import { breakpoint } from '@/theme';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
const ContentContainer = styled.div`
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
@ -9,5 +10,6 @@ const ContentContainer = styled.div`
|
||||||
${tw`mx-auto`};
|
${tw`mx-auto`};
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
ContentContainer.displayName = 'ContentContainer';
|
||||||
|
|
||||||
export default ContentContainer;
|
export default ContentContainer;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import styled from 'styled-components/macro';
|
||||||
import styled from 'styled-components';
|
import tw from 'twin.macro';
|
||||||
|
import Fade from '@/components/elements/Fade';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -12,76 +13,95 @@ export const DropdownButtonRow = styled.button<{ danger?: boolean }>`
|
||||||
transition: 150ms all ease;
|
transition: 150ms all ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
${props => props.danger
|
${props => props.danger ? tw`text-red-700 bg-red-100` : tw`text-neutral-700 bg-neutral-100`};
|
||||||
? tw`text-red-700 bg-red-100`
|
|
||||||
: tw`text-neutral-700 bg-neutral-100`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DropdownMenu = ({ renderToggle, children }: Props) => {
|
interface State {
|
||||||
const menu = useRef<HTMLDivElement>(null);
|
posX: number;
|
||||||
const [ posX, setPosX ] = useState(0);
|
visible: boolean;
|
||||||
const [ visible, setVisible ] = useState(false);
|
}
|
||||||
|
|
||||||
const onClickHandler = (e: React.MouseEvent<any, MouseEvent>) => {
|
class DropdownMenu extends React.PureComponent<Props, State> {
|
||||||
e.preventDefault();
|
menu = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
!visible && setPosX(e.clientX);
|
state: State = {
|
||||||
setVisible(s => !s);
|
posX: 0,
|
||||||
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const windowListener = (e: MouseEvent) => {
|
componentWillUnmount () {
|
||||||
if (e.button === 2 || !visible || !menu.current) {
|
this.removeListeners();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.target === menu.current || menu.current.contains(e.target as Node)) {
|
componentDidUpdate (prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||||
return;
|
const menu = this.menu.current;
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target !== menu.current && !menu.current.contains(e.target as Node)) {
|
if (this.state.visible && !prevState.visible && menu) {
|
||||||
setVisible(false);
|
document.addEventListener('click', this.windowListener);
|
||||||
}
|
document.addEventListener('contextmenu', this.contextMenuListener);
|
||||||
};
|
menu.setAttribute(
|
||||||
|
'style', `left: ${Math.round(this.state.posX - menu.clientWidth)}px`,
|
||||||
useEffect(() => {
|
|
||||||
if (!visible || !menu.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('click', windowListener);
|
|
||||||
menu.current.setAttribute(
|
|
||||||
'style', `left: ${Math.round(posX - menu.current.clientWidth)}px`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('click', windowListener);
|
|
||||||
}
|
}
|
||||||
}, [ visible ]);
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
this.triggerMenu(e.clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 || menu.contains(e.target as Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.target !== menu && !menu.contains(e.target as Node)) {
|
||||||
|
this.setState({ visible: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
triggerMenu = (posX: number) => this.setState(s => ({
|
||||||
|
posX: !s.visible ? posX : s.posX,
|
||||||
|
visible: !s.visible,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{renderToggle(onClickHandler)}
|
{this.props.renderToggle(this.onClickHandler)}
|
||||||
<CSSTransition
|
<Fade timeout={150} in={this.state.visible} unmountOnExit>
|
||||||
timeout={250}
|
|
||||||
in={visible}
|
|
||||||
unmountOnExit={true}
|
|
||||||
classNames={'fade'}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref={menu}
|
ref={this.menu}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setVisible(false);
|
this.setState({ visible: false });
|
||||||
}}
|
}}
|
||||||
className={'absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48'}
|
css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
|
||||||
>
|
>
|
||||||
{children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
</CSSTransition>
|
</Fade>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default DropdownMenu;
|
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 { Field as FormikField, FieldProps } from 'formik';
|
||||||
import classNames from 'classnames';
|
import Input from '@/components/elements/Input';
|
||||||
|
import Label from '@/components/elements/Label';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -12,21 +13,20 @@ interface OwnProps {
|
||||||
|
|
||||||
type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>;
|
type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>;
|
||||||
|
|
||||||
const Field = ({ id, name, light = false, label, description, validate, className, ...props }: Props) => (
|
const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, label, description, validate, ...props }, ref) => (
|
||||||
<FormikField name={name} validate={validate}>
|
<FormikField innerRef={ref} name={name} validate={validate}>
|
||||||
{
|
{
|
||||||
({ field, form: { errors, touched } }: FieldProps) => (
|
({ field, form: { errors, touched } }: FieldProps) => (
|
||||||
<React.Fragment>
|
<>
|
||||||
{label &&
|
{label &&
|
||||||
<label htmlFor={id} className={light ? undefined : 'input-dark-label'}>{label}</label>
|
<Label htmlFor={id} isLight={light}>{label}</Label>
|
||||||
}
|
}
|
||||||
<input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
{...field}
|
{...field}
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames((className || (light ? 'input' : 'input-dark')), {
|
isLight={light}
|
||||||
error: touched[field.name] && errors[field.name],
|
hasError={!!(touched[field.name] && errors[field.name])}
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
{touched[field.name] && errors[field.name] ?
|
{touched[field.name] && errors[field.name] ?
|
||||||
<p className={'input-help error'}>
|
<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
|
description ? <p className={'input-help'}>{description}</p> : null
|
||||||
}
|
}
|
||||||
</React.Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</FormikField>
|
</FormikField>
|
||||||
);
|
));
|
||||||
|
Field.displayName = 'Field';
|
||||||
|
|
||||||
export default Field;
|
export default Field;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Field, FieldProps } from 'formik';
|
import { Field, FieldProps } from 'formik';
|
||||||
import classNames from 'classnames';
|
|
||||||
import InputError from '@/components/elements/InputError';
|
import InputError from '@/components/elements/InputError';
|
||||||
|
import Label from '@/components/elements/Label';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -17,11 +17,11 @@ const FormikFieldWrapper = ({ id, name, label, className, description, validate,
|
||||||
<Field name={name} validate={validate}>
|
<Field name={name} validate={validate}>
|
||||||
{
|
{
|
||||||
({ field, form: { errors, touched } }: FieldProps) => (
|
({ field, form: { errors, touched } }: FieldProps) => (
|
||||||
<div className={classNames(className, { 'has-error': touched[field.name] && errors[field.name] })}>
|
<div className={`${className} ${(touched[field.name] && errors[field.name]) ? 'has-error' : undefined}`}>
|
||||||
{label && <label htmlFor={id} className={'input-dark-label'}>{label}</label>}
|
{label && <Label htmlFor={id}>{label}</Label>}
|
||||||
{children}
|
{children}
|
||||||
<InputError errors={errors} touched={touched} name={field.name}>
|
<InputError errors={errors} touched={touched} name={field.name}>
|
||||||
{description ? <p className={'input-help'}>{description}</p> : null}
|
{description || null}
|
||||||
</InputError>
|
</InputError>
|
||||||
</div>
|
</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 React from 'react';
|
||||||
import capitalize from 'lodash-es/capitalize';
|
|
||||||
import { FormikErrors, FormikTouched } from 'formik';
|
import { FormikErrors, FormikTouched } from 'formik';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import { capitalize } from '@/helpers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
errors: FormikErrors<any>;
|
errors: FormikErrors<any>;
|
||||||
touched: FormikTouched<any>;
|
touched: FormikTouched<any>;
|
||||||
name: string;
|
name: string;
|
||||||
children?: React.ReactNode;
|
children?: string | number | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputError = ({ errors, touched, name, children }: Props) => (
|
const InputError = ({ errors, touched, name, children }: Props) => (
|
||||||
touched[name] && errors[name] ?
|
touched[name] && errors[name] ?
|
||||||
<p className={'input-help error'}>
|
<p css={tw`text-xs text-red-400 pt-2`}>
|
||||||
{typeof errors[name] === 'string' ?
|
{typeof errors[name] === 'string' ?
|
||||||
capitalize(errors[name] as string)
|
capitalize(errors[name] as string)
|
||||||
:
|
:
|
||||||
|
@ -19,9 +20,9 @@ const InputError = ({ errors, touched, name, children }: Props) => (
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
:
|
:
|
||||||
<React.Fragment>
|
<>
|
||||||
{children}
|
{children ? <p css={tw`text-xs text-neutral-400 pt-2`}>{children}</p> : null}
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default InputError;
|
export default InputError;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue