mirror of
https://github.com/pyrohost/pyrodactyl.git
synced 2026-04-06 04:01:58 +02:00
feat: a controversial commit
This commit is contained in:
76
.github/docker/entrypoint.sh
vendored
76
.github/docker/entrypoint.sh
vendored
@@ -5,6 +5,10 @@ mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php
|
||||
&& chmod 777 /var/log/panel/logs/ \
|
||||
&& ln -s /app/storage/logs/ /var/log/panel/
|
||||
|
||||
# Ensure proper permissions for Laravel storage directories
|
||||
mkdir -p /app/storage/logs /app/storage/framework/cache /app/storage/framework/sessions /app/storage/framework/views \
|
||||
&& chmod -R 777 /app/storage/
|
||||
|
||||
# Check that user has mounted the /app/var directory
|
||||
if [ ! -d /app/var ]; then
|
||||
echo "You must mount the /app/var directory to the container."
|
||||
@@ -101,76 +105,8 @@ else
|
||||
echo -e "Skipping database seeding (SKIP_SEED=True)"
|
||||
fi
|
||||
|
||||
# Setup development environment if specified
|
||||
(
|
||||
source /app/.env
|
||||
if [ "$PYRODACTYL_DOCKER_DEV" = "true" ] && [ "$DEV_SETUP" != "true" ]; then
|
||||
echo -e "\e[42mDevelopment environment detected, setting up development resources...\e[0m"
|
||||
|
||||
# Create a developer user
|
||||
php artisan p:user:make -n --email dev@pyro.host --username dev --name-first Developer --name-last User --password password
|
||||
mariadb -u root -h database -p"$DB_ROOT_PASSWORD" --ssl=0 -e "USE panel; UPDATE users SET root_admin = 1;" # workaround because --admin is broken
|
||||
|
||||
# Make a location and node for the panel
|
||||
php artisan p:location:make -n --short local --long Local
|
||||
php artisan p:node:make -n --name local --description "Development Node" --locationId 1 --fqdn localhost --internal-fqdn $WINGS_INTERNAL_IP --public 1 --scheme http --proxy 0 --maxMemory 1024 --maxDisk 10240 --overallocateMemory 0 --overallocateDisk 0
|
||||
|
||||
echo "Adding dummy allocations..."
|
||||
mariadb -u root -h database -p"$DB_ROOT_PASSWORD" --ssl=0 -e "USE panel; INSERT INTO allocations (node_id, ip, port) VALUES (1, '0.0.0.0', 25565), (1, '0.0.0.0', 25566), (1, '0.0.0.0', 25567);"
|
||||
|
||||
echo "Creating database user..."
|
||||
mariadb -u root -h database -p"$DB_ROOT_PASSWORD" --ssl=0 -e "CREATE USER 'pterodactyluser'@'%' IDENTIFIED BY 'somepassword'; GRANT ALL PRIVILEGES ON *.* TO 'pterodactyluser'@'%' WITH GRANT OPTION;"
|
||||
|
||||
# Configure node
|
||||
export WINGS_CONFIG=/etc/pterodactyl/config.yml
|
||||
mkdir -p $(dirname $WINGS_CONFIG)
|
||||
echo "Fetching and modifying Wings configuration file..."
|
||||
CONFIG=$(php artisan p:node:configuration 1)
|
||||
|
||||
# Allow all origins for CORS
|
||||
CONFIG=$(printf "%s\nallowed_origins: ['*']" "$CONFIG")
|
||||
|
||||
# Update Wings configuration paths if WINGS_DIR is set
|
||||
if [ -z "$WINGS_DIR" ]; then
|
||||
echo "WINGS_DIR is not set, using default paths."
|
||||
else
|
||||
echo "Updating Wings configuration paths to '$WINGS_DIR'..."
|
||||
|
||||
# add system section if it doesn't exist
|
||||
if ! echo "$CONFIG" | grep -q "^system:"; then
|
||||
CONFIG=$(printf "%s\nsystem:" "$CONFIG")
|
||||
fi
|
||||
|
||||
update_config() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
# update existing key or add new one
|
||||
if echo "$CONFIG" | grep -q "^ $key:"; then
|
||||
CONFIG=$(echo "$CONFIG" | sed "s|^ $key:.*| $key: $value|")
|
||||
else
|
||||
CONFIG=$(echo "$CONFIG" | sed "/^system:/a\\ $key: $value")
|
||||
fi
|
||||
}
|
||||
|
||||
update_config "root_directory" "$WINGS_DIR/srv/wings/"
|
||||
update_config "log_directory" "$WINGS_DIR/srv/wings/logs/"
|
||||
update_config "data" "$WINGS_DIR/srv/wings/volumes"
|
||||
update_config "archive_directory" "$WINGS_DIR/srv/wings/archives"
|
||||
update_config "backup_directory" "$WINGS_DIR/srv/wings/backups"
|
||||
update_config "tmp_directory" "$WINGS_DIR/srv/wings/tmp/"
|
||||
fi
|
||||
|
||||
echo "Saving Wings configuration file to '$WINGS_CONFIG'..."
|
||||
echo "$CONFIG" > $WINGS_CONFIG
|
||||
|
||||
# Mark setup as complete
|
||||
echo "DEV_SETUP=true" >> /app/.env
|
||||
echo "Development setup complete."
|
||||
elif [ "$DEV_SETUP" = "true" ]; then
|
||||
echo "Skipping development setup, already completed."
|
||||
fi
|
||||
)
|
||||
# Development setup is now handled by the DevelopmentSeeder
|
||||
# Run: php artisan db:seed --class=DevelopmentSeeder
|
||||
|
||||
## start cronjobs for the queue
|
||||
echo -e "Starting cron jobs."
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ public/assets/manifest.json
|
||||
|
||||
# For local development with docker
|
||||
docker-compose.yml
|
||||
local_docker
|
||||
/srv
|
||||
/var
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ class Node extends Model
|
||||
* Fields that are mass assignable.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'public',
|
||||
'name',
|
||||
'location_id',
|
||||
@@ -102,6 +103,8 @@ class Node extends Model
|
||||
'daemonBase',
|
||||
'daemonSFTP',
|
||||
'daemonListen',
|
||||
'daemon_token_id',
|
||||
'daemon_token',
|
||||
'description',
|
||||
'maintenance_mode',
|
||||
];
|
||||
|
||||
@@ -13,5 +13,10 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$this->call(NestSeeder::class);
|
||||
$this->call(EggSeeder::class);
|
||||
|
||||
// Run development seeder in local environment
|
||||
if (app()->environment('local')) {
|
||||
$this->call(DevelopmentSeeder::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
410
database/Seeders/DevelopmentSeeder.php
Normal file
410
database/Seeders/DevelopmentSeeder.php
Normal file
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Location;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Pterodactyl\Models\DatabaseHost;
|
||||
use Pterodactyl\Models\Allocation;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Egg;
|
||||
use Pterodactyl\Services\Servers\ServerCreationService;
|
||||
|
||||
class DevelopmentSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds for development environment.
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
// Only run in development environment
|
||||
if (!app()->environment('local')) {
|
||||
$this->command->info('Development seeder only runs in local environment.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->command->info('Setting up development environment...');
|
||||
|
||||
// Create dev user
|
||||
$user = $this->createDevUser();
|
||||
$this->command->info('✓ Created dev user (username: dev, password: dev)');
|
||||
|
||||
// Create location
|
||||
$location = $this->createLocation();
|
||||
$this->command->info('✓ Created development location');
|
||||
|
||||
// Create Wings node
|
||||
$node = $this->createNode($location);
|
||||
$this->command->info('✓ Created Wings node');
|
||||
|
||||
// Create allocations for the node
|
||||
$this->createAllocations($node);
|
||||
$this->command->info('✓ Created allocations for Wings node');
|
||||
|
||||
// Create database host
|
||||
$databaseHost = $this->createDatabaseHost($node);
|
||||
$this->command->info('✓ Created database host');
|
||||
|
||||
// Generate Wings configuration
|
||||
$this->generateWingsConfiguration($node);
|
||||
$this->command->info('✓ Generated Wings configuration');
|
||||
|
||||
// Create testing Minecraft server
|
||||
$server = $this->createTestingMinecraftServer($user, $node);
|
||||
$this->command->info('✓ Created testing Minecraft server');
|
||||
|
||||
$this->command->info('Development environment setup complete!');
|
||||
$this->command->info('Panel URL: http://localhost:3000');
|
||||
$this->command->info('Username: dev');
|
||||
$this->command->info('Password: dev');
|
||||
if ($server) {
|
||||
$this->command->info('Testing Server: ' . $server->name . ' (UUID: ' . $server->uuidShort . ')');
|
||||
}
|
||||
}
|
||||
|
||||
private function createDevUser(): User
|
||||
{
|
||||
// First try to find existing user by email or username
|
||||
$user = User::where('email', 'dev@pyrodactyl.local')
|
||||
->orWhere('username', 'dev')
|
||||
->first();
|
||||
|
||||
if ($user) {
|
||||
// Update existing user
|
||||
$user->update([
|
||||
'username' => 'dev',
|
||||
'email' => 'dev@pyrodactyl.local',
|
||||
'password' => Hash::make('dev'),
|
||||
'root_admin' => true,
|
||||
'language' => 'en',
|
||||
'use_totp' => false,
|
||||
]);
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Create new user
|
||||
return User::create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'username' => 'dev',
|
||||
'email' => 'dev@pyrodactyl.local',
|
||||
'name_first' => 'Development',
|
||||
'name_last' => 'User',
|
||||
'password' => Hash::make('dev'),
|
||||
'root_admin' => true,
|
||||
'language' => 'en',
|
||||
'use_totp' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createLocation(): Location
|
||||
{
|
||||
return Location::updateOrCreate(
|
||||
['short' => 'dev'],
|
||||
[
|
||||
'long' => 'Development Location',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function createNode(Location $location): Node
|
||||
{
|
||||
// Generate proper daemon token (64 characters as required by Node model)
|
||||
$daemonTokenId = 'dev_token_id_16c'; // 16 characters as required
|
||||
$daemonToken = 'dev_token_64_chars_fixed_for_development_environment_testing'; // 64 characters
|
||||
|
||||
// Check if node already exists
|
||||
$existingNode = Node::where('name', 'Development Node')->first();
|
||||
|
||||
if ($existingNode) {
|
||||
// Update existing node with proper values including UUID if missing
|
||||
$updateData = [
|
||||
'location_id' => $location->id,
|
||||
'fqdn' => 'localhost', // Public FQDN for browser websocket connections
|
||||
'internal_fqdn' => 'wings', // Internal FQDN for panel-to-Wings communication
|
||||
'scheme' => 'http',
|
||||
'behind_proxy' => false,
|
||||
'public' => true,
|
||||
'maintenance_mode' => false,
|
||||
'memory' => 8192,
|
||||
'memory_overallocate' => 0,
|
||||
'disk' => 102400,
|
||||
'disk_overallocate' => 0,
|
||||
'upload_size' => 100,
|
||||
'daemonListen' => 8080,
|
||||
'daemonSFTP' => 2022,
|
||||
'daemonBase' => '/var/lib/pterodactyl/volumes',
|
||||
'daemon_token_id' => $daemonTokenId,
|
||||
'daemon_token' => encrypt($daemonToken),
|
||||
'description' => 'Development Wings node for local testing',
|
||||
];
|
||||
|
||||
// Set UUID if it's missing
|
||||
if (empty($existingNode->uuid)) {
|
||||
$updateData['uuid'] = (string) \Illuminate\Support\Str::uuid();
|
||||
$this->command->info("Setting UUID for existing node: {$updateData['uuid']}");
|
||||
}
|
||||
|
||||
$existingNode->update($updateData);
|
||||
return $existingNode;
|
||||
}
|
||||
|
||||
// Create new node with UUID
|
||||
$nodeUuid = (string) \Illuminate\Support\Str::uuid();
|
||||
$node = Node::create([
|
||||
'uuid' => $nodeUuid,
|
||||
'name' => 'Development Node',
|
||||
'location_id' => $location->id,
|
||||
'fqdn' => 'localhost', // Public FQDN for browser websocket connections
|
||||
'internal_fqdn' => 'wings', // Internal FQDN for panel-to-Wings communication
|
||||
'scheme' => 'http',
|
||||
'behind_proxy' => false,
|
||||
'public' => true,
|
||||
'maintenance_mode' => false,
|
||||
'memory' => 8192,
|
||||
'memory_overallocate' => 0,
|
||||
'disk' => 102400,
|
||||
'disk_overallocate' => 0,
|
||||
'upload_size' => 100,
|
||||
'daemonListen' => 8080,
|
||||
'daemonSFTP' => 2022,
|
||||
'daemonBase' => '/var/lib/pterodactyl/volumes',
|
||||
'daemon_token_id' => $daemonTokenId,
|
||||
'daemon_token' => encrypt($daemonToken),
|
||||
'description' => 'Development Wings node for local testing',
|
||||
]);
|
||||
|
||||
$this->command->info("Created node with UUID: {$nodeUuid}");
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function createAllocations(Node $node): void
|
||||
{
|
||||
// Create a range of allocations for testing
|
||||
$ports = [25565, 25566, 25567, 25568, 25569, 8080, 8081, 8082, 8083, 8084];
|
||||
|
||||
foreach ($ports as $port) {
|
||||
Allocation::updateOrCreate(
|
||||
[
|
||||
'node_id' => $node->id,
|
||||
'ip' => '0.0.0.0',
|
||||
'port' => $port,
|
||||
],
|
||||
[
|
||||
'ip_alias' => null,
|
||||
'server_id' => null,
|
||||
'notes' => 'Development allocation',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function createDatabaseHost(Node $node): DatabaseHost
|
||||
{
|
||||
// For development, use root user with full privileges
|
||||
$rootPassword = env('DB_ROOT_PASSWORD', 'rootpassword');
|
||||
|
||||
return DatabaseHost::updateOrCreate(
|
||||
['name' => 'Development Database'],
|
||||
[
|
||||
'host' => 'database',
|
||||
'port' => 3306,
|
||||
'username' => 'root',
|
||||
'password' => encrypt($rootPassword),
|
||||
'max_databases' => 100,
|
||||
'node_id' => $node->id, // Link to development node
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function generateWingsConfiguration(Node $node): void
|
||||
{
|
||||
try {
|
||||
$configPath = '/etc/pterodactyl/config.yml';
|
||||
|
||||
// Ensure the directory exists
|
||||
$configDir = dirname($configPath);
|
||||
if (!File::exists($configDir)) {
|
||||
File::makeDirectory($configDir, 0755, true);
|
||||
}
|
||||
|
||||
// Generate the Wings configuration using the node's built-in method
|
||||
$this->command->info('Generating Wings configuration...');
|
||||
|
||||
// Get the configuration directly from the node model
|
||||
$configContent = $node->getYamlConfiguration();
|
||||
|
||||
// Add CORS configuration for development
|
||||
$configContent .= "\nallowed_origins: ['*']\n";
|
||||
|
||||
// Update paths for Docker development environment if WINGS_DIR is set
|
||||
$wingsDir = env('WINGS_DIR');
|
||||
if ($wingsDir) {
|
||||
$this->command->info("Updating Wings paths for Docker development environment: {$wingsDir}");
|
||||
|
||||
// Add system configuration section with proper Docker paths
|
||||
$systemConfig = "\nsystem:\n";
|
||||
$systemConfig .= " root_directory: {$wingsDir}/wings/\n";
|
||||
$systemConfig .= " log_directory: {$wingsDir}/wings/logs/\n";
|
||||
$systemConfig .= " data: {$wingsDir}/wings/volumes\n";
|
||||
$systemConfig .= " archive_directory: {$wingsDir}/wings/archives\n";
|
||||
$systemConfig .= " backup_directory: {$wingsDir}/wings/backups\n";
|
||||
$systemConfig .= " tmp_directory: {$wingsDir}/wings/tmp/\n";
|
||||
$systemConfig .= " sftp:\n";
|
||||
$systemConfig .= " bind_port: {$node->daemonSFTP}\n";
|
||||
|
||||
$configContent .= $systemConfig;
|
||||
}
|
||||
|
||||
// Write the configuration file
|
||||
File::put($configPath, $configContent);
|
||||
|
||||
$this->command->info("Wings configuration saved to: {$configPath}");
|
||||
|
||||
// Also save a backup copy for debugging
|
||||
$backupPath = storage_path('logs/wings-config-backup.yml');
|
||||
File::put($backupPath, $configContent);
|
||||
$this->command->info("Backup configuration saved to: {$backupPath}");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->command->error("Failed to generate Wings configuration: " . $e->getMessage());
|
||||
$this->command->info("You may need to manually configure Wings after setup.");
|
||||
|
||||
// Log the full error for debugging
|
||||
Log::error('Wings configuration generation failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'node_id' => $node->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function createTestingMinecraftServer(User $user, Node $node): ?Server
|
||||
{
|
||||
// Check if testing server already exists
|
||||
$existingServer = Server::where('name', 'Testing Minecraft Server')->first();
|
||||
if ($existingServer) {
|
||||
$this->command->info('Testing Minecraft server already exists, skipping creation.');
|
||||
return $existingServer;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get an available allocation
|
||||
$allocation = Allocation::where('node_id', $node->id)
|
||||
->whereNull('server_id')
|
||||
->first();
|
||||
|
||||
if (!$allocation) {
|
||||
$this->command->error('No available allocations for testing server.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the Vanilla Minecraft egg - try multiple possible names
|
||||
$egg = Egg::whereIn('name', ['Vanilla Minecraft', 'Minecraft', 'Paper', 'Forge'])
|
||||
->first();
|
||||
|
||||
if (!$egg) {
|
||||
$this->command->error('No suitable Minecraft egg found. Available eggs:');
|
||||
$availableEggs = Egg::pluck('name')->toArray();
|
||||
$this->command->info('Available: ' . implode(', ', $availableEggs));
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->command->info("Using egg: {$egg->name}");
|
||||
|
||||
// Get the default Docker image
|
||||
$dockerImages = $egg->docker_images;
|
||||
if (empty($dockerImages)) {
|
||||
$this->command->error('No Docker images available for the selected egg.');
|
||||
return null;
|
||||
}
|
||||
|
||||
$defaultImage = is_array($dockerImages) ? array_values($dockerImages)[0] : $dockerImages;
|
||||
|
||||
// Prepare server creation data with specified requirements
|
||||
$serverData = [
|
||||
'name' => 'Testing Minecraft Server',
|
||||
'description' => 'Development testing server with 4 cores, 4GB RAM, 32GB storage',
|
||||
'owner_id' => $user->id,
|
||||
'memory' => 4096, // 4GB RAM
|
||||
'overhead_memory' => 2048, // 2GB overhead memory
|
||||
'swap' => 0,
|
||||
'disk' => 32768, // 32GB storage (in MB)
|
||||
'io' => 500,
|
||||
'cpu' => 400, // 4 cores (100% per core)
|
||||
'threads' => null,
|
||||
'allocation_id' => $allocation->id,
|
||||
'node_id' => $node->id,
|
||||
'nest_id' => $egg->nest_id,
|
||||
'egg_id' => $egg->id,
|
||||
'startup' => $egg->startup,
|
||||
'image' => $defaultImage,
|
||||
'database_limit' => 8, // 8 database slots
|
||||
'allocation_limit' => 8, // 8 allocation slots
|
||||
'backup_limit' => 8, // 8 backup slots
|
||||
'skip_scripts' => false,
|
||||
'oom_disabled' => true,
|
||||
'start_on_completion' => false, // Don't auto-start in development to avoid issues
|
||||
'environment' => [
|
||||
'SERVER_JARFILE' => 'server.jar',
|
||||
'VANILLA_VERSION' => 'latest',
|
||||
],
|
||||
];
|
||||
|
||||
// Use the proper ServerCreationService to create and provision the server
|
||||
$serverCreationService = app(ServerCreationService::class);
|
||||
|
||||
$this->command->info('Creating server with ServerCreationService...');
|
||||
$server = $serverCreationService->handle($serverData);
|
||||
|
||||
// Verify and fix allocation assignment if needed
|
||||
$this->ensureAllocationAssignment($server, $allocation);
|
||||
|
||||
$this->command->info("Server created successfully with ID: {$server->id}");
|
||||
return $server;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->command->error("Failed to create testing Minecraft server: " . $e->getMessage());
|
||||
Log::error('Server creation failed in development seeder', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'user_id' => $user->id,
|
||||
'node_id' => $node->id,
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the allocation is properly assigned to the server.
|
||||
* This fixes potential race conditions in the ServerCreationService.
|
||||
*/
|
||||
private function ensureAllocationAssignment(Server $server, Allocation $allocation): void
|
||||
{
|
||||
// Refresh the allocation from database to get current state
|
||||
$allocation->refresh();
|
||||
|
||||
// If allocation is not assigned to the server, fix it
|
||||
if ($allocation->server_id !== $server->id) {
|
||||
$this->command->info('Fixing allocation assignment...');
|
||||
$allocation->server_id = $server->id;
|
||||
$allocation->save();
|
||||
$this->command->info("Allocation {$allocation->ip}:{$allocation->port} assigned to server {$server->id}");
|
||||
}
|
||||
|
||||
// Verify the server can see its allocations
|
||||
$serverAllocations = $server->allocations()->count();
|
||||
if ($serverAllocations === 0) {
|
||||
$this->command->warn('Server has no allocations after creation, this may cause issues.');
|
||||
} else {
|
||||
$this->command->info("Server has {$serverAllocations} allocation(s) properly assigned.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ x-common:
|
||||
# A list of valid timezones can be found here: http://php.net/manual/en/timezones.php
|
||||
APP_TIMEZONE: 'UTC'
|
||||
APP_SERVICE_AUTHOR: 'noreply@example.com'
|
||||
DB_USERNAME: 'pterodactyl'
|
||||
DB_USERNAME: 'root'
|
||||
# DB_PASSWORD: "Uncomment this to user your own password"
|
||||
|
||||
# Uncomment the line below and set to a non-empty value if you want to use Let's Encrypt
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
restart: always
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
volumes:
|
||||
- './srv/database:/var/lib/mysql'
|
||||
- './local_docker/database:/var/lib/mysql'
|
||||
environment:
|
||||
<<: *db-environment
|
||||
MYSQL_DATABASE: 'panel'
|
||||
@@ -47,7 +47,11 @@ services:
|
||||
image: redis:alpine
|
||||
restart: always
|
||||
panel:
|
||||
image: pyrodactyl:develop
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
args:
|
||||
DEV: "true"
|
||||
restart: always
|
||||
ports:
|
||||
- '3000:80'
|
||||
@@ -57,20 +61,21 @@ services:
|
||||
- cache
|
||||
volumes:
|
||||
- '.:/app'
|
||||
- './srv/var:/app/var'
|
||||
- './srv/nginx/:/etc/nginx/http.d/'
|
||||
- './srv/certs:/etc/letsencrypt'
|
||||
- './srv/logs/:/app/storage/logs'
|
||||
- './srv/pterodactyl/config/:/etc/pterodactyl'
|
||||
- './local_docker/var:/app/var'
|
||||
- './local_docker/nginx/:/etc/nginx/http.d/'
|
||||
- './local_docker/certs:/etc/letsencrypt'
|
||||
- './local_docker/logs/:/app/storage/logs'
|
||||
- './local_docker/pterodactyl/config/:/etc/pterodactyl'
|
||||
# volumes set for the dirs below so the image's files are used instead of the host's files
|
||||
- panel_vendor:/app/vendor
|
||||
- panel_storage:/app/storage
|
||||
- panel_bootstrap_cache:/app/bootstrap/cache
|
||||
environment:
|
||||
<<: [*panel-environment, *mail-environment]
|
||||
DB_PASSWORD: *db-password
|
||||
DB_PASSWORD: *db-root-password
|
||||
DB_ROOT_PASSWORD: *db-root-password
|
||||
APP_ENV: 'production'
|
||||
APP_ENV: 'local'
|
||||
APP_DEBUG: 'true'
|
||||
APP_ENVIRONMENT_ONLY: 'false'
|
||||
CACHE_DRIVER: 'redis'
|
||||
SESSION_DRIVER: 'redis'
|
||||
@@ -81,14 +86,14 @@ services:
|
||||
DB_PORT: '3306'
|
||||
HASHIDS_LENGTH: 8
|
||||
WINGS_INTERNAL_IP: 'wings'
|
||||
WINGS_DIR: '${PWD}'
|
||||
WINGS_DIR: '${PWD}/local_docker'
|
||||
PYRODACTYL_DOCKER_DEV: 'true'
|
||||
wings:
|
||||
# The default Wings image doesn't work on macOS Docker
|
||||
# This fork simply removes the incompatible `io.priority` cgroup v2 flag
|
||||
# For Linux users, you can use the default image by uncommenting the line below
|
||||
# image: ghcr.io/pterodactyl/wings:latest
|
||||
image: ghcr.io/he3als/wings-mac:latest
|
||||
image: ghcr.io/pterodactyl/wings:latest
|
||||
# image: ghcr.io/he3als/wings-mac:latest
|
||||
restart: always
|
||||
ports:
|
||||
- '8080:8080'
|
||||
@@ -102,20 +107,20 @@ services:
|
||||
volumes:
|
||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||
- '/etc/ssl/certs:/etc/ssl/certs:ro'
|
||||
- './srv/pterodactyl/config/:/etc/pterodactyl/'
|
||||
- './local_docker/pterodactyl/config/:/etc/pterodactyl/'
|
||||
|
||||
# The volumes below need to be the exact same path in the container as on the host.
|
||||
#
|
||||
# The paths are currently hardcoded in the container on first run, meaning if you move
|
||||
# this repo on your host, you'll need to delete "srv" folder so the paths can be recreated.
|
||||
# this repo on your host, you'll need to delete "local_docker" folder so the paths can be recreated.
|
||||
#
|
||||
# If you change these from $PWD, make sure to update `WINGS_DIR` in the panel service too.
|
||||
# Do not change anything but the $PWD part as this is also hardcoded in the container.
|
||||
|
||||
- '${PWD}/srv/wings/tmp/:${PWD}/srv/wings/tmp/'
|
||||
- '${PWD}/srv/wings/docker/containers/:${PWD}/srv/wings/docker/containers/'
|
||||
- '${PWD}/srv/wings/:${PWD}/srv/wings/'
|
||||
- '${PWD}/srv/wings/logs/:${PWD}/srv/wings/logs/'
|
||||
- '${PWD}/local_docker/wings/tmp/:${PWD}/local_docker/wings/tmp/'
|
||||
- '${PWD}/local_docker/wings/docker/containers/:${PWD}/local_docker/wings/docker/containers/'
|
||||
- '${PWD}/local_docker/wings/:${PWD}/local_docker/wings/'
|
||||
- '${PWD}/local_docker/wings/logs/:${PWD}/local_docker/wings/logs/'
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import Pagination from '@/components/elements/Pagination';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/elements/Tabs';
|
||||
import { PageListContainer } from '@/components/elements/pages/PageList';
|
||||
|
||||
import getServers from '@/api/getServers';
|
||||
import { PaginatedResult } from '@/api/http';
|
||||
@@ -75,7 +76,13 @@ const DashboardContainer = () => {
|
||||
<div className='flex gap-4'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className='flex items-center gap-2 font-bold text-sm px-3 py-1 rounded-md bg-[#ffffff11] hover:bg-[#ffffff22] transition hover:duration-0 cursor-pointer'>
|
||||
<button
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(124.75% 124.75% at 50.01% -10.55%, rgb(36, 36, 36) 0%, rgb(20, 20, 20) 100%)',
|
||||
}}
|
||||
className='flex items-center gap-2 font-bold text-sm px-4 py-3 rounded-full border-[1px] border-[#ffffff12] hover:bg-[#ffffff11] transition-colors duration-150 cursor-pointer'
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='20'
|
||||
@@ -152,45 +159,69 @@ const DashboardContainer = () => {
|
||||
</div>
|
||||
</MainPageHeader>
|
||||
{!servers ? (
|
||||
<></>
|
||||
<div className='flex items-center justify-center py-12'>
|
||||
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-brand'></div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<TabsContent value='list'>
|
||||
<Pagination data={servers} onPageSelect={setPage}>
|
||||
{({ items }) =>
|
||||
items.length > 0 ? (
|
||||
items.map((server, index) => (
|
||||
<div
|
||||
key={server.uuid}
|
||||
className='transform-gpu skeleton-anim-2 mb-4'
|
||||
style={{
|
||||
animationDelay: `${index * 50 + 50}ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<ServerRow className='flex-row' key={server.uuid} server={server} />
|
||||
</div>
|
||||
))
|
||||
<PageListContainer>
|
||||
{items.map((server, index) => (
|
||||
<div
|
||||
key={server.uuid}
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
animationDelay: `${index * 50 + 50}ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<ServerRow className='flex-row' key={server.uuid} server={server} />
|
||||
</div>
|
||||
))}
|
||||
</PageListContainer>
|
||||
) : (
|
||||
<p className={`text-center text-sm text-zinc-400`}>
|
||||
{showOnlyAdmin
|
||||
? 'There are no other servers to display.'
|
||||
: 'There are no servers associated with your account.'}
|
||||
</p>
|
||||
<div className='flex flex-col items-center justify-center py-12 px-4'>
|
||||
<div className='text-center'>
|
||||
<div className='w-16 h-16 mx-auto mb-4 rounded-full bg-[#ffffff11] flex items-center justify-center'>
|
||||
<svg
|
||||
className='w-8 h-8 text-zinc-400'
|
||||
fill='currentColor'
|
||||
viewBox='0 0 20 20'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className='text-lg font-medium text-zinc-200 mb-2'>
|
||||
{showOnlyAdmin ? 'No other servers found' : 'No servers found'}
|
||||
</h3>
|
||||
<p className='text-sm text-zinc-400 max-w-sm'>
|
||||
{showOnlyAdmin
|
||||
? 'There are no other servers to display.'
|
||||
: 'There are no servers associated with your account.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Pagination>
|
||||
</TabsContent>
|
||||
<TabsContent value='grid'>
|
||||
<div className='grid grid-cols-2 gap-x-4'>
|
||||
<Pagination data={servers} onPageSelect={setPage}>
|
||||
{({ items }) =>
|
||||
items.length > 0 ? (
|
||||
items.map((server, index) => (
|
||||
<Pagination data={servers} onPageSelect={setPage}>
|
||||
{({ items }) =>
|
||||
items.length > 0 ? (
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
{items.map((server, index) => (
|
||||
<div
|
||||
key={server.uuid}
|
||||
className='transform-gpu skeleton-anim-2 mb-4 w-full'
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
animationDelay: `${index * 50 + 50}ms`,
|
||||
animationTimingFunction:
|
||||
@@ -203,17 +234,36 @@ const DashboardContainer = () => {
|
||||
server={server}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className={`text-center text-sm text-zinc-400`}>
|
||||
{showOnlyAdmin
|
||||
? 'There are no other servers to display.'
|
||||
: 'There are no servers associated with your account.'}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</Pagination>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col items-center justify-center py-12 px-4'>
|
||||
<div className='text-center'>
|
||||
<div className='w-16 h-16 mx-auto mb-4 rounded-full bg-[#ffffff11] flex items-center justify-center'>
|
||||
<svg
|
||||
className='w-8 h-8 text-zinc-400'
|
||||
fill='currentColor'
|
||||
viewBox='0 0 16 17'
|
||||
>
|
||||
<path
|
||||
d='M1 3.1C1 2.54 1 2.26 1.109 2.046C1.20487 1.85785 1.35785 1.70487 1.546 1.609C1.76 1.5 2.04 1.5 2.6 1.5H5.4C5.96 1.5 6.24 1.5 6.454 1.609C6.64215 1.70487 6.79513 1.85785 6.891 2.046C7 2.26 7 2.54 7 3.1V3.9C7 4.46 7 4.74 6.891 4.954C6.79513 5.14215 6.64215 5.29513 6.454 5.391C6.24 5.5 5.96 5.5 5.4 5.5H2.6C2.04 5.5 1.76 5.5 1.546 5.391C1.35785 5.29513 1.20487 5.14215 1.109 4.954C1 4.74 1 4.46 1 3.9V3.1ZM9 3.1C9 2.54 9 2.26 9.109 2.046C9.20487 1.85785 9.35785 1.70487 9.546 1.609C9.76 1.5 10.04 1.5 10.6 1.5H13.4C13.96 1.5 14.24 1.5 14.454 1.609C14.6422 1.70487 14.7951 1.85785 14.891 2.046C15 2.26 15 2.54 15 3.1V3.9C15 4.46 15 4.74 14.891 4.954C14.7951 5.14215 14.6422 5.29513 14.454 5.391C14.24 5.5 13.96 5.5 13.4 5.5H10.6C10.04 5.5 9.76 5.5 9.546 5.391C9.35785 5.29513 9.20487 5.14215 9.109 4.954C9 4.74 9 4.46 9 3.9V3.1ZM1 8.1C1 7.54 1 7.26 1.109 7.046C1.20487 6.85785 1.35785 6.70487 1.546 6.609C1.76 6.5 2.04 6.5 2.6 6.5H5.4C5.96 6.5 6.24 6.5 6.454 6.609C6.64215 6.70487 6.79513 6.85785 6.891 7.046C7 7.26 7 7.54 7 8.1V8.9C7 9.46 7 9.74 6.891 9.954C6.79513 10.1422 6.64215 10.2951 6.454 10.391C6.24 10.5 5.96 10.5 5.4 10.5H2.6C2.04 10.5 1.76 10.5 1.546 10.391C1.35785 10.2951 1.20487 10.1422 1.109 9.954C1 9.74 1 9.46 1 8.9V8.1ZM9 8.1C9 7.54 9 7.26 9.109 7.046C9.20487 6.85785 9.35785 6.70487 9.546 6.609C9.76 6.5 10.04 6.5 10.6 6.5H13.4C13.96 6.5 14.24 6.5 14.454 6.609C14.6422 6.70487 14.7951 6.85785 14.891 7.046C15 7.26 15 7.54 15 8.1V8.9C15 9.46 15 9.74 14.891 9.954C14.7951 10.1422 14.6422 10.2951 14.454 10.391C14.24 10.5 13.96 10.5 13.4 10.5H10.6C10.04 10.5 9.76 10.5 9.546 10.391C9.35785 10.2951 9.20487 10.1422 9.109 9.954C9 9.74 9 9.46 9 8.9V8.1ZM1 13.1C1 12.54 1 12.26 1.109 12.046C1.20487 11.8578 1.35785 11.7049 1.546 11.609C1.76 11.5 2.04 11.5 2.6 11.5H5.4C5.96 11.5 6.24 11.5 6.454 11.609C6.64215 11.7049 6.79513 11.8578 6.891 12.046C7 12.26 7 12.54 7 13.1V13.9C7 14.46 7 14.74 6.891 14.954C6.79513 15.1422 6.64215 15.2951 6.454 15.391C6.24 15.5 5.96 15.5 5.4 15.5H2.6C2.04 15.5 1.76 15.5 1.546 15.391C1.35785 15.2951 1.20487 15.1422 1.109 14.954C1 14.74 1 14.46 1 13.9V13.1ZM9 13.1C9 12.54 9 12.26 9.109 12.046C9.20487 11.8578 9.35785 11.7049 9.546 11.609C9.76 11.5 10.04 11.5 10.6 11.5H13.4C13.96 11.5 14.24 11.5 14.454 11.609C14.6422 11.7049 14.7951 11.8578 14.891 12.046C15 12.26 15 12.54 15 13.1V13.9C15 14.46 15 14.74 14.891 14.954C14.7951 15.1422 14.6422 15.2951 14.454 15.391C14.24 15.5 13.96 15.5 13.4 15.5H10.6C10.04 15.5 9.76 15.5 9.546 15.391C9.35785 15.2951 9.20487 15.1422 9.109 14.954C9 14.74 9 14.46 9 13.9V13.1Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className='text-lg font-medium text-zinc-200 mb-2'>
|
||||
{showOnlyAdmin ? 'No other servers found' : 'No servers found'}
|
||||
</h3>
|
||||
<p className='text-sm text-zinc-400 max-w-sm'>
|
||||
{showOnlyAdmin
|
||||
? 'There are no other servers to display.'
|
||||
: 'There are no servers associated with your account.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Pagination>
|
||||
</TabsContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -12,9 +12,8 @@ import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/ser
|
||||
const isAlarmState = (current: number, limit: number): boolean => limit > 0 && current / (limit * 1024 * 1024) >= 0.9;
|
||||
|
||||
const StatusIndicatorBox = styled.div<{ $status: ServerPowerState | undefined }>`
|
||||
// background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.044) 100%);
|
||||
border: 1px solid #ffffff07;
|
||||
background: #ffffff11;
|
||||
border: 1px solid #ffffff12;
|
||||
transition: all 250ms ease-in-out;
|
||||
padding: 1.75rem 2rem;
|
||||
cursor: pointer;
|
||||
@@ -25,8 +24,9 @@ const StatusIndicatorBox = styled.div<{ $status: ServerPowerState | undefined }>
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid #ffffff11;
|
||||
background: #ffffff18;
|
||||
border: 1px solid #ffffff19;
|
||||
background: #ffffff19;
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
& .status-bar {
|
||||
@@ -121,7 +121,11 @@ const ServerRow = ({ server, className }: { server: Server; className?: string }
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`h-full hidden sm:flex items-center justify-center bg-[#ffffff09] border-[1px] border-[#ffffff11] shadow-xs rounded-md w-fit whitespace-nowrap px-4 py-2 text-sm gap-4`}
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(124.75% 124.75% at 50.01% -10.55%, rgb(36, 36, 36) 0%, rgb(20, 20, 20) 100%)',
|
||||
}}
|
||||
className={`h-full hidden sm:flex items-center justify-center border-[1px] border-[#ffffff12] shadow-md rounded-lg w-fit whitespace-nowrap px-4 py-2 text-sm gap-4`}
|
||||
>
|
||||
{!stats || isSuspended ? (
|
||||
isSuspended ? (
|
||||
@@ -151,44 +155,27 @@ const ServerRow = ({ server, className }: { server: Server; className?: string }
|
||||
<Fragment>
|
||||
<div className={`sm:flex hidden`}>
|
||||
<div className={`flex justify-center gap-2 w-fit`}>
|
||||
<p className='text-sm text-[#ffffff66] font-bold w-fit whitespace-nowrap'>CPU</p>
|
||||
<p className='font-bold w-fit whitespace-nowrap'>{stats.cpuUsagePercent.toFixed(2)}%</p>
|
||||
<p className='text-xs text-zinc-400 font-medium w-fit whitespace-nowrap'>CPU</p>
|
||||
<p className='text-xs font-bold w-fit whitespace-nowrap'>
|
||||
{stats.cpuUsagePercent.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
{/* <p className={`text-xs text-zinc-600 text-center mt-1`}>of {cpuLimit}</p> */}
|
||||
</div>
|
||||
<div className={`sm:flex hidden`}>
|
||||
{/* <p className={`text-xs text-zinc-600 text-center mt-1`}>of {memoryLimit}</p> */}
|
||||
<div className={`flex justify-center gap-2 w-fit`}>
|
||||
<p className='text-sm text-[#ffffff66] font-bold w-fit whitespace-nowrap'>RAM</p>
|
||||
<p className='font-bold w-fit whitespace-nowrap'>
|
||||
<p className='text-xs text-zinc-400 font-medium w-fit whitespace-nowrap'>RAM</p>
|
||||
<p className='text-xs font-bold w-fit whitespace-nowrap'>
|
||||
{bytesToString(stats.memoryUsageInBytes, 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`sm:flex hidden`}>
|
||||
<div className={`flex justify-center gap-2 w-fit`}>
|
||||
<p className='text-sm text-[#ffffff66] font-bold w-fit whitespace-nowrap'>Storage</p>
|
||||
<p className='font-bold w-fit whitespace-nowrap'>
|
||||
<p className='text-xs text-zinc-400 font-medium w-fit whitespace-nowrap'>Storage</p>
|
||||
<p className='text-xs font-bold w-fit whitespace-nowrap'>
|
||||
{bytesToString(stats.diskUsageInBytes, 0)}
|
||||
</p>
|
||||
</div>
|
||||
{/* Pyro has unlimited storage */}
|
||||
{/* ░░░░░▄▄▄▄▀▀▀▀▀▀▀▀▄▄▄▄▄▄░░░░░░░
|
||||
░░░░░█░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░▀▀▄░░░░
|
||||
░░░░█░░░▒▒▒▒▒▒░░░░░░░░▒▒▒░░█░░░
|
||||
░░░█░░░░░░▄██▀▄▄░░░░░▄▄▄░░░░█░░
|
||||
░▄▀▒▄▄▄▒░█▀▀▀▀▄▄█░░░██▄▄█░░░░█░
|
||||
█░▒█▒▄░▀▄▄▄▀░░░░░░░░█░░░▒▒▒▒▒░█
|
||||
█░▒█░█▀▄▄░░░░░█▀░░░░▀▄░░▄▀▀▀▄▒█
|
||||
░█░▀▄░█▄░█▀▄▄░▀░▀▀░▄▄▀░░░░█░░█░
|
||||
░░█░░░▀▄▀█▄▄░█▀▀▀▄▄▄▄▀▀█▀██░█░░
|
||||
░░░█░░░░██░░▀█▄▄▄█▄▄█▄████░█░░░
|
||||
░░░░█░░░░▀▀▄░█░░░█░█▀██████░█░░
|
||||
░░░░░▀▄░░░░░▀▀▄▄▄█▄█▄█▄█▄▀░░█░░
|
||||
░░░░░░░▀▄▄░▒▒▒▒░░░░░░░░░░▒░░░█░
|
||||
░░░░░░░░░░▀▀▄▄░▒▒▒▒▒▒▒▒▒▒░░░░█░
|
||||
░░░░░░░░░░░░░░▀▄▄▄▄▄░░░░░░░░█░░ */}
|
||||
{/* <p className={`text-xs text-zinc-600 text-center mt-1`}>of {diskLimit}</p> */}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { JSX } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HeaderWrapper = styled.div``;
|
||||
@@ -22,7 +23,7 @@ export const MainPageHeader: React.FC<MainPageHeaderProps> = ({
|
||||
'flex',
|
||||
direction === 'row' ? 'items-center flex-col md:flex-row' : 'items-start flex-col',
|
||||
'justify-between',
|
||||
'mb-8 gap-8 mt-8 md:mt-0 select-none',
|
||||
'mb-4 gap-8 mt-8 md:mt-0 select-none',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-4 flex-wrap'>
|
||||
|
||||
@@ -35,18 +35,23 @@ const BackupContainer = () => {
|
||||
if (!backups || (error && isValidating)) {
|
||||
return (
|
||||
<ServerContentBlock title={'Backups'}>
|
||||
<h1 className='text-[52px] font-extrabold leading-[98%] tracking-[-0.14rem]'>Backups</h1>
|
||||
<FlashMessageRender byKey={'backups'} />
|
||||
<MainPageHeader title={'Backups'} />
|
||||
<div className='flex items-center justify-center py-12'>
|
||||
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-brand'></div>
|
||||
</div>
|
||||
</ServerContentBlock>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ServerContentBlock title={'Backups'}>
|
||||
<FlashMessageRender byKey={'backups'} />
|
||||
<MainPageHeader title={'Backups'}>
|
||||
<Can action={'backup.create'}>
|
||||
<div className={`flex flex-col sm:flex-row items-center justify-end`}>
|
||||
<div className='flex flex-col sm:flex-row items-center justify-end gap-4'>
|
||||
{backupLimit > 0 && backups.backupCount > 0 && (
|
||||
<p className={`text-sm text-zinc-300 mb-4 sm:mr-6 sm:mb-0 text-right`}>
|
||||
<p className='text-sm text-zinc-300 text-center sm:text-right'>
|
||||
{backups.backupCount} of {backupLimit} backups
|
||||
</p>
|
||||
)}
|
||||
@@ -54,19 +59,31 @@ const BackupContainer = () => {
|
||||
</div>
|
||||
</Can>
|
||||
</MainPageHeader>
|
||||
<FlashMessageRender byKey={'backups'} />
|
||||
|
||||
<Pagination data={backups} onPageSelect={setPage}>
|
||||
{({ items }) =>
|
||||
!items.length ? (
|
||||
// Don't show any error messages if the server has no backups and the user cannot
|
||||
// create additional ones for the server.
|
||||
!backupLimit ? null : (
|
||||
<p className={`text-center text-sm text-zinc-300`}>
|
||||
{page > 1
|
||||
? "Looks like we've run out of backups to show you, try going back a page."
|
||||
: 'Your server does not have any backups.'}
|
||||
</p>
|
||||
)
|
||||
<div className='flex flex-col items-center justify-center py-12 px-4'>
|
||||
<div className='text-center'>
|
||||
<div className='w-16 h-16 mx-auto mb-4 rounded-full bg-[#ffffff11] flex items-center justify-center'>
|
||||
<svg className='w-8 h-8 text-zinc-400' fill='currentColor' viewBox='0 0 20 20'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className='text-lg font-medium text-zinc-200 mb-2'>
|
||||
{backupLimit > 0 ? 'No backups found' : 'Backups unavailable'}
|
||||
</h3>
|
||||
<p className='text-sm text-zinc-400 max-w-sm'>
|
||||
{backupLimit > 0
|
||||
? 'Your server does not have any backups. Create one to get started.'
|
||||
: 'Backups cannot be created for this server.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PageListContainer>
|
||||
{items.map((backup) => (
|
||||
@@ -76,9 +93,6 @@ const BackupContainer = () => {
|
||||
)
|
||||
}
|
||||
</Pagination>
|
||||
{backupLimit === 0 && (
|
||||
<p className={`text-center text-sm text-zinc-300`}>Backups cannot be created for this server.</p>
|
||||
)}
|
||||
</ServerContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import Can from '@/components/elements/Can';
|
||||
import { ContextMenuContent, ContextMenuItem } from '@/components/elements/ContextMenu';
|
||||
import Input from '@/components/elements/Input';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import HugeIconsCloudUp from '@/components/elements/hugeicons/CloudUp';
|
||||
import HugeIconsDelete from '@/components/elements/hugeicons/Delete';
|
||||
import HugeIconsFileDownload from '@/components/elements/hugeicons/FileDownload';
|
||||
import HugeIconsFileSecurity from '@/components/elements/hugeicons/FileSecurity';
|
||||
@@ -160,40 +160,61 @@ const BackupContextMenu = ({ backup }: Props) => {
|
||||
</Dialog.Confirm>
|
||||
<SpinnerOverlay visible={loading} fixed />
|
||||
{backup.isSuccessful ? (
|
||||
<ContextMenuContent className='flex flex-col gap-1'>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Can action={'backup.download'}>
|
||||
<ContextMenuItem className='flex gap-2' onSelect={doDownload}>
|
||||
<HugeIconsFileDownload className='h-4! w-4!' fill='currentColor' />
|
||||
Download Backup
|
||||
</ContextMenuItem>
|
||||
<button
|
||||
type='button'
|
||||
onClick={doDownload}
|
||||
disabled={loading}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-[#ffffff19] rounded-lg text-sm text-zinc-300 hover:text-zinc-100 transition-colors duration-150 disabled:opacity-50'
|
||||
>
|
||||
<HugeIconsFileDownload className='h-4 w-4' fill='currentColor' />
|
||||
<span className='hidden sm:inline'>Download</span>
|
||||
</button>
|
||||
</Can>
|
||||
<Can action={'backup.restore'}>
|
||||
<ContextMenuItem className='flex gap-2' onSelect={() => setModal('restore')}>
|
||||
<HugeIconsFileDownload className='h-4! w-4!' fill='currentColor' />
|
||||
Restore Backup
|
||||
</ContextMenuItem>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setModal('restore')}
|
||||
disabled={loading}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-[#ffffff19] rounded-lg text-sm text-zinc-300 hover:text-zinc-100 transition-colors duration-150 disabled:opacity-50'
|
||||
>
|
||||
<HugeIconsCloudUp className='h-4 w-4' fill='currentColor' />
|
||||
<span className='hidden sm:inline'>Restore</span>
|
||||
</button>
|
||||
</Can>
|
||||
<Can action={'backup.delete'}>
|
||||
<>
|
||||
<ContextMenuItem className='flex gap-2' onClick={onLockToggle}>
|
||||
<HugeIconsFileSecurity className='h-4! w-4!' fill='currentColor' />
|
||||
{backup.isLocked ? 'Unlock' : 'Lock'}
|
||||
</ContextMenuItem>
|
||||
{!backup.isLocked && (
|
||||
<ContextMenuItem className='flex gap-2' onSelect={() => setModal('delete')}>
|
||||
<HugeIconsDelete className='h-4! w-4!' fill='currentColor' />
|
||||
Delete Backup
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</>
|
||||
<button
|
||||
type='button'
|
||||
onClick={onLockToggle}
|
||||
disabled={loading}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-[#ffffff19] rounded-lg text-sm text-zinc-300 hover:text-zinc-100 transition-colors duration-150 disabled:opacity-50'
|
||||
>
|
||||
<HugeIconsFileSecurity className='h-4 w-4' fill='currentColor' />
|
||||
<span className='hidden sm:inline'>{backup.isLocked ? 'Unlock' : 'Lock'}</span>
|
||||
</button>
|
||||
{!backup.isLocked && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setModal('delete')}
|
||||
disabled={loading}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-red-600/20 rounded-lg text-sm text-zinc-300 hover:text-red-400 transition-colors duration-150 disabled:opacity-50'
|
||||
>
|
||||
<HugeIconsDelete className='h-4 w-4' fill='currentColor' />
|
||||
<span className='hidden sm:inline'>Delete</span>
|
||||
</button>
|
||||
)}
|
||||
</Can>
|
||||
</ContextMenuContent>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setModal('delete')}
|
||||
className={`text-zinc-200 transition-colors duration-150 hover:text-zinc-100 p-2 cursor-pointer`}
|
||||
disabled={loading}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-red-600/20 rounded-lg text-sm text-zinc-300 hover:text-red-400 transition-colors duration-150 disabled:opacity-50'
|
||||
>
|
||||
Delete Backup
|
||||
<HugeIconsDelete className='h-4 w-4' fill='currentColor' />
|
||||
<span className='hidden sm:inline'>Delete</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -53,65 +53,67 @@ const BackupRow = ({ backup }: Props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<PageListItem>
|
||||
<div className={`flex-auto max-w-full box-border`}>
|
||||
<div className='flex flex-row align-middle items-center gap-6 truncate'>
|
||||
<div className='flex-none'>
|
||||
{backup.completedAt === null ? (
|
||||
<Spinner size={'small'} />
|
||||
) : backup.isLocked ? (
|
||||
<FontAwesomeIcon icon={faLock} className='text-red-500' />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faFile} />
|
||||
<div className='bg-linear-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff15] p-4 sm:p-5 rounded-xl hover:border-[#ffffff20] transition-all'>
|
||||
<div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4'>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-3 mb-2'>
|
||||
<div className='flex-shrink-0 w-8 h-8 rounded-lg bg-[#ffffff11] flex items-center justify-center'>
|
||||
{backup.completedAt === null ? (
|
||||
<Spinner size={'small'} />
|
||||
) : backup.isLocked ? (
|
||||
<FontAwesomeIcon icon={faLock} className='text-red-400 w-4 h-4' />
|
||||
) : backup.isSuccessful ? (
|
||||
<FontAwesomeIcon icon={faFile} className='text-green-400 w-4 h-4' />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faFile} className='text-red-400 w-4 h-4' />
|
||||
)}
|
||||
</div>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-2 mb-1'>
|
||||
{backup.completedAt !== null && !backup.isSuccessful && (
|
||||
<span className='bg-red-500 py-1 px-2 rounded-full text-white text-xs uppercase font-medium'>
|
||||
Failed
|
||||
</span>
|
||||
)}
|
||||
<h3 className='text-base font-medium text-zinc-100 truncate'>{backup.name}</h3>
|
||||
{backup.isLocked && (
|
||||
<span className='text-xs text-red-400 font-medium bg-red-500/10 px-2 py-1 rounded'>
|
||||
Locked
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex items-center w-full md:flex-1`}>
|
||||
<div className={`flex flex-col`}>
|
||||
<div className={`flex items-center text-sm mb-1`}>
|
||||
{backup.completedAt !== null && !backup.isSuccessful && (
|
||||
<span
|
||||
className={`bg-red-500 py-px px-2 rounded-full text-white text-xs uppercase border border-red-600 mr-2`}
|
||||
>
|
||||
Failed
|
||||
</span>
|
||||
)}
|
||||
<div className={`flex gap-2 items-center justify-center`}>
|
||||
<p className='break-words truncate text-lg'>{backup.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
{backup.checksum && (
|
||||
<p className={`mt-1 md:mt-0 text-xs text-zinc-400 font-mono truncate`}>
|
||||
{backup.checksum}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{backup.checksum && (
|
||||
<p className='text-sm text-zinc-400 font-mono truncate'>{backup.checksum}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row justify-center font-medium sm:justify-between min-w-full lg:w-96 sm:min-w-40'>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm'>
|
||||
{backup.completedAt !== null && backup.isSuccessful && (
|
||||
<>
|
||||
<span className={`text-xs sm:flex-initial sm:ml-0`}>{bytesToString(backup.bytes)}</span>
|
||||
<p className={`text-xs inline sm:hidden`}>, </p>
|
||||
</>
|
||||
<div>
|
||||
<p className='text-xs text-zinc-500 uppercase tracking-wide mb-1'>Size</p>
|
||||
<p className='text-zinc-300 font-medium'>{bytesToString(backup.bytes)}</p>
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
title={format(backup.createdAt, 'ddd, MMMM do, yyyy HH:mm:ss')}
|
||||
className={`text-xs sm:flex-initial`}
|
||||
>
|
||||
{formatDistanceToNow(backup.createdAt, { includeSeconds: true, addSuffix: true })}
|
||||
</p>
|
||||
<div>
|
||||
<p className='text-xs text-zinc-500 uppercase tracking-wide mb-1'>Created</p>
|
||||
<p
|
||||
className='text-zinc-300 font-medium'
|
||||
title={format(backup.createdAt, 'ddd, MMMM do, yyyy HH:mm:ss')}
|
||||
>
|
||||
{formatDistanceToNow(backup.createdAt, { includeSeconds: true, addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 sm:flex-col sm:gap-3'>
|
||||
<Can action={['backup.download', 'backup.restore', 'backup.delete']} matchAny>
|
||||
{!backup.completedAt ? <></> : <BackupContextMenu backup={backup} />}
|
||||
{backup.completedAt ? <BackupContextMenu backup={backup} /> : null}
|
||||
</Can>
|
||||
</PageListItem>
|
||||
</ContextMenuTrigger>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ const CreateBackupButton = () => {
|
||||
(data) => ({ ...data!, items: data!.items.concat(backup), backupCount: data!.backupCount + 1 }),
|
||||
false,
|
||||
);
|
||||
setSubmitting(false);
|
||||
setVisible(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -122,7 +123,7 @@ const CreateBackupButton = () => {
|
||||
background:
|
||||
'radial-gradient(124.75% 124.75% at 50.01% -10.55%, rgb(36, 36, 36) 0%, rgb(20, 20, 20) 100%)',
|
||||
}}
|
||||
className='px-8 py-3 border-[1px] border-[#ffffff12] rounded-full text-sm font-bold shadow-md cursor-pointer'
|
||||
className='px-8 py-3 border-[1px] border-[#ffffff12] rounded-full text-sm font-bold shadow-md cursor-pointer hover:bg-[#ffffff11] transition-colors duration-150'
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
New Backup
|
||||
|
||||
@@ -10,11 +10,13 @@ interface ChartBlockProps {
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export default ({ title, legend, children }: ChartBlockProps) => (
|
||||
<div className={clsx(styles.chart_container, 'group p-8!')}>
|
||||
<div className={'flex items-center justify-between mb-4'}>
|
||||
<h3 className={'font-extrabold text-sm'}>{title}</h3>
|
||||
{legend && <div className={'text-sm flex items-center'}>{legend}</div>}
|
||||
<div className='bg-gradient-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff12] rounded-xl p-3 sm:p-4 hover:border-[#ffffff20] transition-all duration-150 group h-full shadow-sm'>
|
||||
<div className={'flex items-center justify-between mb-3 sm:mb-4'}>
|
||||
<h3 className={'font-semibold text-sm text-zinc-100 group-hover:text-white transition-colors duration-150'}>
|
||||
{title}
|
||||
</h3>
|
||||
{legend && <div className={'text-xs sm:text-sm flex items-center text-zinc-400'}>{legend}</div>}
|
||||
</div>
|
||||
<div className={'z-10 overflow-hidden rounded-lg'}>{children}</div>
|
||||
<div className={'z-10 overflow-hidden rounded-lg h-40 sm:h-48'}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -45,16 +45,22 @@ const terminalProps: ITerminalOptions = {
|
||||
disableStdin: true,
|
||||
cursorStyle: 'underline',
|
||||
allowTransparency: true,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace, monospace',
|
||||
// rows: 30,
|
||||
fontSize: window.innerWidth < 640 ? 11 : 12,
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
|
||||
theme: theme,
|
||||
};
|
||||
|
||||
const Console = () => {
|
||||
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pyrodactyl~ \u001b[0m';
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const terminal = useMemo(() => new Terminal({ ...terminalProps, rows: 30 }), []);
|
||||
const terminal = useMemo(
|
||||
() =>
|
||||
new Terminal({
|
||||
...terminalProps,
|
||||
rows: window.innerWidth < 640 ? 20 : 25,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const fitAddon = new FitAddon();
|
||||
const searchAddon = new SearchAddon();
|
||||
const webLinksAddon = new WebLinksAddon();
|
||||
@@ -145,6 +151,9 @@ const Console = () => {
|
||||
'resize',
|
||||
debounce(() => {
|
||||
if (terminal.element) {
|
||||
// Update font size based on window width
|
||||
const newFontSize = window.innerWidth < 640 ? 11 : 12;
|
||||
terminal.options.fontSize = newFontSize;
|
||||
fitAddon.fit();
|
||||
}
|
||||
}, 100),
|
||||
@@ -193,39 +202,25 @@ const Console = () => {
|
||||
}, [connected, instance]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
animationDelay: `250ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<div className={clsx(styles.terminal, 'relative')}>
|
||||
<div className='bg-gradient-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff12] rounded-xl hover:border-[#ffffff20] transition-all duration-150 overflow-hidden shadow-sm'>
|
||||
<div className='relative'>
|
||||
<SpinnerOverlay visible={!connected} size={'large'} />
|
||||
<div
|
||||
className={clsx(styles.terminalContainer, styles.overflows_container, {
|
||||
'rounded-b': !canSendCommands,
|
||||
})}
|
||||
>
|
||||
<div className={'h-full'}>
|
||||
<div id={styles.terminal} ref={ref} />
|
||||
<div className='bg-[#131313] min-h-[280px] sm:min-h-[380px] p-3 sm:p-4 font-mono overflow-hidden'>
|
||||
<div className='h-full w-full'>
|
||||
<div ref={ref} className='h-full w-full' />
|
||||
</div>
|
||||
</div>
|
||||
{canSendCommands && (
|
||||
<div className={clsx('relative', styles.overflows_container)}>
|
||||
<div className='relative border-t-[1px] border-[#ffffff11] bg-[#0f0f0f]'>
|
||||
<input
|
||||
className={clsx('peer', styles.command_input)}
|
||||
type={'text'}
|
||||
placeholder={'Enter a command'}
|
||||
aria-label={'Console command input.'}
|
||||
className='w-full bg-transparent px-3 py-2.5 sm:px-4 sm:py-3 font-mono text-xs sm:text-sm text-zinc-100 placeholder-zinc-500 border-0 outline-none focus:ring-0 focus:outline-none focus:bg-[#1a1a1a] transition-colors duration-150'
|
||||
type='text'
|
||||
placeholder='Enter a command...'
|
||||
aria-label='Console command input.'
|
||||
disabled={!instance || !connected}
|
||||
onKeyDown={handleCommandKeyDown}
|
||||
autoCorrect={'off'}
|
||||
autoCapitalize={'none'}
|
||||
autoCorrect='off'
|
||||
autoCapitalize='none'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { faChartBar, faServer, faTerminal } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { memo } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
@@ -16,13 +18,11 @@ import { ServerContext } from '@/state/server';
|
||||
|
||||
import Features from '@feature/Features';
|
||||
|
||||
import { StatusPill } from './StatusPill';
|
||||
|
||||
export type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
|
||||
|
||||
const ServerConsoleContainer = () => {
|
||||
const name = ServerContext.useStoreState((state) => state.server.data!.name);
|
||||
const description = ServerContext.useStoreState((state) => state.server.data!.description);
|
||||
// const description = ServerContext.useStoreState((state) => state.server.data!.description);
|
||||
const isInstalling = ServerContext.useStoreState((state) => state.server.isInstalling);
|
||||
const isTransferring = ServerContext.useStoreState((state) => state.server.data!.isTransferring);
|
||||
const eggFeatures = ServerContext.useStoreState((state) => state.server.data!.eggFeatures, isEqual);
|
||||
@@ -30,34 +30,134 @@ const ServerConsoleContainer = () => {
|
||||
|
||||
return (
|
||||
<ServerContentBlock title={'Home'}>
|
||||
<div className='w-full h-full min-h-full flex-1 flex flex-col gap-4'>
|
||||
<div className='w-full h-full min-h-full flex-1 flex flex-col px-2 sm:px-0'>
|
||||
{(isNodeUnderMaintenance || isInstalling || isTransferring) && (
|
||||
<Alert type={'warning'} className={'mb-4'}>
|
||||
{isNodeUnderMaintenance
|
||||
? 'The node of this server is currently under maintenance and all actions are unavailable.'
|
||||
: isInstalling
|
||||
? 'This server is currently running its installation process and most actions are unavailable.'
|
||||
: 'This server is currently being transferred to another node and all actions are unavailable.'}
|
||||
</Alert>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2 mb-3 sm:mb-4'
|
||||
style={{
|
||||
animationDelay: '50ms',
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<Alert type={'warning'}>
|
||||
{isNodeUnderMaintenance
|
||||
? 'The node of this server is currently under maintenance and all actions are unavailable.'
|
||||
: isInstalling
|
||||
? 'This server is currently running its installation process and most actions are unavailable.'
|
||||
: 'This server is currently being transferred to another node and all actions are unavailable.'}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<MainPageHeader title={name} titleChildren={<StatusPill />}>
|
||||
<PowerButtons className='skeleton-anim-2 duration-75 flex gap-1 items-center justify-center' />
|
||||
</MainPageHeader>
|
||||
{description && (
|
||||
<h2 className='text-sm -mt-8'>
|
||||
<span className='opacity-50'>{description}</span>
|
||||
</h2>
|
||||
)}
|
||||
<ServerDetailsBlock />
|
||||
<Console />
|
||||
<div className={'grid grid-cols-1 md:grid-cols-3 gap-4'}>
|
||||
<Spinner.Suspense>
|
||||
<StatGraphs />
|
||||
</Spinner.Suspense>
|
||||
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2 mb-3 sm:mb-4'
|
||||
style={{
|
||||
animationDelay: '75ms',
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<MainPageHeader title={name}>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
animationDelay: '100ms',
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<PowerButtons className='flex gap-1 items-center justify-center' />
|
||||
</div>
|
||||
</MainPageHeader>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-3 sm:gap-4'>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
animationDelay: '125ms',
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<div className='bg-gradient-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff12] rounded-xl p-3 sm:p-4 hover:border-[#ffffff20] transition-all duration-150 shadow-sm'>
|
||||
<div className='flex items-center gap-2 sm:gap-3 mb-3 sm:mb-4'>
|
||||
<div className='w-5 h-5 sm:w-6 sm:h-6 rounded-lg bg-[#ffffff11] flex items-center justify-center'>
|
||||
<FontAwesomeIcon
|
||||
icon={faServer}
|
||||
className='w-2.5 h-2.5 sm:w-3 sm:h-3 text-zinc-400'
|
||||
/>
|
||||
</div>
|
||||
<h3 className='text-sm sm:text-base font-semibold text-zinc-100'>Server Resources</h3>
|
||||
</div>
|
||||
<ServerDetailsBlock />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
animationDelay: '175ms',
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<div className='bg-gradient-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff12] rounded-xl p-3 sm:p-4 hover:border-[#ffffff20] transition-all duration-150 shadow-sm'>
|
||||
<div className='flex items-center gap-2 sm:gap-3 mb-3 sm:mb-4'>
|
||||
<div className='w-5 h-5 sm:w-6 sm:h-6 rounded-lg bg-[#ffffff11] flex items-center justify-center'>
|
||||
<FontAwesomeIcon
|
||||
icon={faTerminal}
|
||||
className='w-2.5 h-2.5 sm:w-3 sm:h-3 text-zinc-400'
|
||||
/>
|
||||
</div>
|
||||
<h3 className='text-sm sm:text-base font-semibold text-zinc-100'>Console</h3>
|
||||
</div>
|
||||
<Console />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
animationDelay: '225ms',
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<div className='bg-gradient-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff12] rounded-xl p-3 sm:p-4 hover:border-[#ffffff20] transition-all duration-150 shadow-sm'>
|
||||
<div className='flex items-center gap-2 sm:gap-3 mb-3 sm:mb-4'>
|
||||
<div className='w-5 h-5 sm:w-6 sm:h-6 rounded-lg bg-[#ffffff11] flex items-center justify-center'>
|
||||
<FontAwesomeIcon
|
||||
icon={faChartBar}
|
||||
className='w-2.5 h-2.5 sm:w-3 sm:h-3 text-zinc-400'
|
||||
/>
|
||||
</div>
|
||||
<h3 className='text-sm sm:text-base font-semibold text-zinc-100'>
|
||||
Performance Metrics
|
||||
</h3>
|
||||
</div>
|
||||
<div className={'grid grid-cols-1 md:grid-cols-3 gap-3 sm:gap-4'}>
|
||||
<Spinner.Suspense>
|
||||
<StatGraphs />
|
||||
</Spinner.Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
animationDelay: '275ms',
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<Features enabled={eggFeatures} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<Features enabled={eggFeatures} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</ServerContentBlock>
|
||||
);
|
||||
|
||||
@@ -79,13 +79,11 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={clsx('flex md:flex-row gap-4 flex-col', className)}>
|
||||
<div className={clsx('grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4', className)}>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
animationDelay: `150ms`,
|
||||
animationDelay: `50ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
@@ -97,9 +95,7 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
animationDelay: `175ms`,
|
||||
animationDelay: `75ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
@@ -115,9 +111,7 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
animationDelay: `200ms`,
|
||||
animationDelay: `100ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
@@ -133,9 +127,7 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
animationDelay: `225ms`,
|
||||
animationDelay: `125ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
|
||||
@@ -14,10 +14,21 @@ interface StatBlockProps {
|
||||
const StatBlock = ({ title, copyOnClick, className, children }: StatBlockProps) => {
|
||||
return (
|
||||
<CopyOnClick text={copyOnClick}>
|
||||
<div className={clsx(styles.stat_block, 'bg-[#ffffff09] border-[1px] border-[#ffffff11]', className)}>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-gradient-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff12] rounded-xl p-3 sm:p-4 hover:border-[#ffffff20] transition-all duration-150 cursor-pointer group shadow-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={'flex flex-col justify-center overflow-hidden w-full'}>
|
||||
<p className={'leading-tight text-xs md:text-sm text-zinc-400'}>{title}</p>
|
||||
<div className={'text-[32px] font-extrabold leading-[98%] tracking-[-0.07rem] w-full truncate'}>
|
||||
<p className={'leading-tight text-xs text-zinc-400 mb-2 uppercase tracking-wide font-medium'}>
|
||||
{title}
|
||||
</p>
|
||||
<div
|
||||
className={
|
||||
'text-lg sm:text-xl font-bold leading-tight tracking-tight w-full truncate text-zinc-100 group-hover:text-white transition-colors duration-150'
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,7 @@ const CreateDatabaseButton = () => {
|
||||
})
|
||||
.then((database) => {
|
||||
appendDatabase(database);
|
||||
setSubmitting(false);
|
||||
setVisible(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -106,7 +107,7 @@ const CreateDatabaseButton = () => {
|
||||
background:
|
||||
'radial-gradient(124.75% 124.75% at 50.01% -10.55%, rgb(36, 36, 36) 0%, rgb(20, 20, 20) 100%)',
|
||||
}}
|
||||
className='px-8 py-3 border-[1px] border-[#ffffff12] rounded-full text-sm font-bold shadow-md cursor-pointer'
|
||||
className='px-8 py-3 border-[1px] border-[#ffffff12] rounded-full text-sm font-bold shadow-md cursor-pointer hover:bg-[#ffffff11] transition-colors duration-150'
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
New Database
|
||||
|
||||
@@ -161,74 +161,64 @@ const DatabaseRow = ({ database }: Props) => {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Title */}
|
||||
<div className={`flex-auto box-border min-w-fit`}>
|
||||
<div className='flex flex-row flex-none align-middle items-center gap-6'>
|
||||
<FontAwesomeIcon icon={faDatabase} className='flex-none' />
|
||||
<div>
|
||||
<CopyOnClick text={database.name}>
|
||||
<p className='text-lg'>{database.name}</p>
|
||||
</CopyOnClick>
|
||||
<CopyOnClick text={database.connectionString}>
|
||||
<p className={`text-xs text-zinc-400 font-mono`}>{database.connectionString}</p>
|
||||
</CopyOnClick>
|
||||
<div className='bg-linear-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff15] p-4 sm:p-5 rounded-xl hover:border-[#ffffff20] transition-all'>
|
||||
<div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4'>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-3 mb-2'>
|
||||
<div className='flex-shrink-0 w-8 h-8 rounded-lg bg-[#ffffff11] flex items-center justify-center'>
|
||||
<FontAwesomeIcon icon={faDatabase} className='text-zinc-400 w-4 h-4' />
|
||||
</div>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<CopyOnClick text={database.name}>
|
||||
<h3 className='text-base font-medium text-zinc-100 truncate'>{database.name}</h3>
|
||||
</CopyOnClick>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm'>
|
||||
<div>
|
||||
<p className='text-xs text-zinc-500 uppercase tracking-wide mb-1'>Endpoint</p>
|
||||
<CopyOnClick text={database.connectionString}>
|
||||
<p className='text-zinc-300 font-mono truncate'>{database.connectionString}</p>
|
||||
</CopyOnClick>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-xs text-zinc-500 uppercase tracking-wide mb-1'>From</p>
|
||||
<CopyOnClick text={database.allowConnectionsFrom}>
|
||||
<p className='text-zinc-300 font-mono truncate'>{database.allowConnectionsFrom}</p>
|
||||
</CopyOnClick>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-xs text-zinc-500 uppercase tracking-wide mb-1'>Username</p>
|
||||
<CopyOnClick text={database.username}>
|
||||
<p className='text-zinc-300 font-mono truncate'>{database.username}</p>
|
||||
</CopyOnClick>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 sm:flex-col sm:gap-3'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setConnectionVisible(true)}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-[#ffffff19] rounded-lg text-sm text-zinc-300 hover:text-zinc-100 transition-colors duration-150'
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} className='w-4 h-4' />
|
||||
<span className='hidden sm:inline'>Details</span>
|
||||
</button>
|
||||
<Can action={'database.delete'}>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setVisible(true)}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-red-600/20 rounded-lg text-sm text-zinc-300 hover:text-red-400 transition-colors duration-150'
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} className='w-4 h-4' />
|
||||
<span className='hidden sm:inline'>Delete</span>
|
||||
</button>
|
||||
</Can>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Properties + buttons */}
|
||||
<div className={`flex flex-col items-center sm:gap-12 gap-4 sm:flex-row`}>
|
||||
<div className='flex flex-wrap gap-4 justify-center m-auto'>
|
||||
<For
|
||||
each={[
|
||||
{ label: 'Endpoint', value: database.connectionString },
|
||||
{ label: 'From', value: database.allowConnectionsFrom },
|
||||
{ label: 'Username', value: database.username },
|
||||
]}
|
||||
memo
|
||||
>
|
||||
{(db, index) => (
|
||||
<div key={index} className='text-center'>
|
||||
<CopyOnClick text={db.value}>
|
||||
<p className='text-sm'>{db.value}</p>
|
||||
</CopyOnClick>
|
||||
<p className='mt-1 text-xs text-zinc-500 uppercase select-none'>{db.label}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div className='flex align-middle items-center justify-center'>
|
||||
<button
|
||||
type={'button'}
|
||||
aria-label={'View database connection details'}
|
||||
className={`text-sm p-2 text-zinc-500 hover:text-zinc-100 transition-colors duration-150 mr-4 flex align-middle items-center justify-center flex-col cursor-pointer`}
|
||||
onClick={() => setConnectionVisible(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} className={`px-5`} size='lg' />
|
||||
Details
|
||||
</button>
|
||||
<Can action={'database.delete'}>
|
||||
<button
|
||||
type={'button'}
|
||||
aria-label={'Delete database'}
|
||||
className={`text-sm p-2 text-zinc-500 hover:text-red-600 transition-colors duration-150 flex align-middle items-center justify-center flex-col cursor-pointer`}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} className={`px-5`} size='lg' />
|
||||
Delete
|
||||
</button>
|
||||
</Can>
|
||||
</div>
|
||||
{/* <Button onClick={() => setConnectionVisible(true)}>
|
||||
<FontAwesomeIcon icon={faEye} fixedWidth />
|
||||
</Button>
|
||||
<Can action={'database.delete'}>
|
||||
<Button color={'red'} onClick={() => setVisible(true)}>
|
||||
<FontAwesomeIcon icon={faTrashAlt} fixedWidth />
|
||||
</Button>
|
||||
</Can> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,9 +45,9 @@ const DatabasesContainer = () => {
|
||||
<FlashMessageRender byKey={'databases'} />
|
||||
<MainPageHeader title={'Databases'}>
|
||||
<Can action={'database.create'}>
|
||||
<div className={`flex flex-col sm:flex-row items-center justify-end`}>
|
||||
<div className='flex flex-col sm:flex-row items-center justify-end gap-4'>
|
||||
{databaseLimit > 0 && databases.length > 0 && (
|
||||
<p className={`text-sm text-zinc-300 mb-4 sm:mr-6 sm:mb-0 text-right`}>
|
||||
<p className='text-sm text-zinc-300 text-center sm:text-right'>
|
||||
{databases.length} of {databaseLimit} databases
|
||||
</p>
|
||||
)}
|
||||
@@ -56,26 +56,38 @@ const DatabasesContainer = () => {
|
||||
</Can>
|
||||
</MainPageHeader>
|
||||
|
||||
{!databases.length && loading ? null : (
|
||||
<>
|
||||
{databases.length > 0 ? (
|
||||
<PageListContainer data-pyro-backups>
|
||||
<For each={databases} memo>
|
||||
{(database, index) => (
|
||||
<PageListItem key={index}>
|
||||
<DatabaseRow key={database.id} database={database} />
|
||||
</PageListItem>
|
||||
)}
|
||||
</For>
|
||||
</PageListContainer>
|
||||
) : (
|
||||
<p className={`text-center text-sm text-zinc-300`}>
|
||||
{!databases.length && loading ? (
|
||||
<div className='flex items-center justify-center py-12'>
|
||||
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-brand'></div>
|
||||
</div>
|
||||
) : databases.length > 0 ? (
|
||||
<PageListContainer data-pyro-databases>
|
||||
<For each={databases} memo>
|
||||
{(database, index) => <DatabaseRow key={database.id} database={database} />}
|
||||
</For>
|
||||
</PageListContainer>
|
||||
) : (
|
||||
<div className='flex flex-col items-center justify-center py-12 px-4'>
|
||||
<div className='text-center'>
|
||||
<div className='w-16 h-16 mx-auto mb-4 rounded-full bg-[#ffffff11] flex items-center justify-center'>
|
||||
<svg className='w-8 h-8 text-zinc-400' fill='currentColor' viewBox='0 0 20 20'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className='text-lg font-medium text-zinc-200 mb-2'>
|
||||
{databaseLimit > 0 ? 'No databases found' : 'Databases unavailable'}
|
||||
</h3>
|
||||
<p className='text-sm text-zinc-400 max-w-sm'>
|
||||
{databaseLimit > 0
|
||||
? 'Your server does not have any databases.'
|
||||
? 'Your server does not have any databases. Create one to get started.'
|
||||
: 'Databases cannot be created for this server.'}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ServerContentBlock>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import { Textarea } from '@/components/elements/Input';
|
||||
import InputSpinner from '@/components/elements/InputSpinner';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import { PageListItem } from '@/components/elements/pages/PageList';
|
||||
import DeleteAllocationButton from '@/components/server/network/DeleteAllocationButton';
|
||||
|
||||
import { ip } from '@/lib/formatters';
|
||||
@@ -57,62 +56,60 @@ const AllocationRow = ({ allocation }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageListItem>
|
||||
<div className={'flex items-center w-full md:w-auto'}>
|
||||
<div className={'mr-4 flex-1 md:w-40'}>
|
||||
{allocation.alias ? (
|
||||
<CopyOnClick text={allocation.alias}>
|
||||
<div>
|
||||
<Code dark className={'w-40 truncate'}>
|
||||
{allocation.alias}
|
||||
</Code>
|
||||
</div>
|
||||
</CopyOnClick>
|
||||
<div className='bg-linear-to-b from-[#ffffff08] to-[#ffffff05] border-[1px] border-[#ffffff15] p-4 sm:p-5 rounded-xl hover:border-[#ffffff20] transition-all'>
|
||||
<div className='flex flex-col sm:flex-row sm:items-end sm:justify-between gap-3'>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-2 flex-wrap mb-2'>
|
||||
{allocation.alias ? (
|
||||
<CopyOnClick text={allocation.alias}>
|
||||
<h3 className='text-lg font-medium text-zinc-100 font-mono'>{allocation.alias}</h3>
|
||||
</CopyOnClick>
|
||||
) : (
|
||||
<CopyOnClick text={ip(allocation.ip)}>
|
||||
<h3 className='text-lg font-medium text-zinc-100 font-mono'>{ip(allocation.ip)}</h3>
|
||||
</CopyOnClick>
|
||||
)}
|
||||
<span className='text-zinc-500'>:</span>
|
||||
<span className='text-lg font-medium text-zinc-100 font-mono'>{allocation.port}</span>
|
||||
</div>
|
||||
<div className='min-h-[2rem] flex items-end'>
|
||||
<InputSpinner visible={loading}>
|
||||
<Textarea
|
||||
className='w-full bg-transparent border-0 p-0 text-sm text-zinc-400 placeholder-zinc-500 resize-none focus:ring-0 focus:text-zinc-300'
|
||||
placeholder='Add notes for this allocation...'
|
||||
defaultValue={allocation.notes || undefined}
|
||||
onChange={(e) => setAllocationNotes(e.currentTarget.value)}
|
||||
rows={1}
|
||||
/>
|
||||
</InputSpinner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 flex-shrink-0'>
|
||||
{allocation.isDefault ? (
|
||||
<div className='flex items-center justify-center px-4 py-2 bg-brand/20 border border-brand/30 rounded-lg text-sm text-brand font-medium'>
|
||||
Primary Port
|
||||
</div>
|
||||
) : (
|
||||
<CopyOnClick text={ip(allocation.ip)}>
|
||||
<div>
|
||||
<Code dark>{ip(allocation.ip)}</Code>
|
||||
</div>
|
||||
</CopyOnClick>
|
||||
<>
|
||||
<Can action={'allocation.update'}>
|
||||
<button
|
||||
type='button'
|
||||
onClick={setPrimaryAllocation}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-[#ffffff19] rounded-lg text-sm text-zinc-300 hover:text-zinc-100 transition-colors duration-150'
|
||||
>
|
||||
<span className='hidden sm:inline'>Make Primary</span>
|
||||
<span className='sm:hidden'>Primary</span>
|
||||
</button>
|
||||
</Can>
|
||||
<Can action={'allocation.delete'}>
|
||||
<DeleteAllocationButton allocation={allocation.id} />
|
||||
</Can>
|
||||
</>
|
||||
)}
|
||||
<label className='uppercase text-xs mt-1 text-zinc-400 block px-1 select-none transition-colors duration-150'>
|
||||
{allocation.alias ? 'Hostname' : 'IP Address'}
|
||||
</label>
|
||||
</div>
|
||||
<div className={'w-16 md:w-24 overflow-hidden'}>
|
||||
<Code dark>{allocation.port}</Code>
|
||||
<label className='uppercase text-xs mt-1 text-zinc-400 block px-1 select-none transition-colors duration-150'>
|
||||
Port
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'mt-4 w-full md:mt-0 md:flex-1 md:w-auto'}>
|
||||
<InputSpinner visible={loading}>
|
||||
<Textarea
|
||||
className={'bg-transparent p-4 rounded-xl w-full'}
|
||||
placeholder={'Notes'}
|
||||
defaultValue={allocation.notes || undefined}
|
||||
onChange={(e) => setAllocationNotes(e.currentTarget.value)}
|
||||
/>
|
||||
</InputSpinner>
|
||||
</div>
|
||||
<div className={'flex justify-end space-x-4 mt-4 w-full md:mt-0 md:w-48'}>
|
||||
{allocation.isDefault ? (
|
||||
<p>Primary Port</p>
|
||||
) : (
|
||||
<>
|
||||
<Can action={'allocation.delete'}>
|
||||
<DeleteAllocationButton allocation={allocation.id} />
|
||||
</Can>
|
||||
<Can action={'allocation.update'}>
|
||||
<Button.Text size={Button.Sizes.Small} onClick={setPrimaryAllocation}>
|
||||
Make Primary
|
||||
</Button.Text>
|
||||
</Can>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PageListItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import HugeIconsDelete from '@/components/elements/hugeicons/Delete';
|
||||
|
||||
import deleteServerAllocation from '@/api/server/network/deleteServerAllocation';
|
||||
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||
@@ -47,8 +48,13 @@ const DeleteAllocationButton = ({ allocation }: Props) => {
|
||||
>
|
||||
This allocation will be immediately removed from your server.
|
||||
</Dialog.Confirm>
|
||||
<button className='cursor-pointer' onClick={() => setConfirm(true)}>
|
||||
Delete
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setConfirm(true)}
|
||||
className='flex items-center justify-center gap-2 px-3 py-2 bg-[#ffffff11] hover:bg-red-600/20 rounded-lg text-sm text-zinc-300 hover:text-red-400 transition-colors duration-150'
|
||||
>
|
||||
<HugeIconsDelete className='h-4 w-4' fill='currentColor' />
|
||||
<span className='hidden sm:inline'>Delete</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { For } from 'million/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import Can from '@/components/elements/Can';
|
||||
import { MainPageHeader } from '@/components/elements/MainPageHeader';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
@@ -54,14 +55,15 @@ const NetworkContainer = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ServerContentBlock showFlashKey={'server:network'} title={'Network'}>
|
||||
<ServerContentBlock title={'Network'}>
|
||||
<FlashMessageRender byKey={'server:network'} />
|
||||
<MainPageHeader title={'Network'}>
|
||||
{!data ? null : (
|
||||
<>
|
||||
{allocationLimit > 0 && (
|
||||
<Can action={'allocation.create'}>
|
||||
<div className={`flex flex-col sm:flex-row items-center justify-end`}>
|
||||
<p className={`text-sm text-zinc-300 mb-4 sm:mr-6 sm:mb-0 text-right`}>
|
||||
<div className='flex flex-col sm:flex-row items-center justify-end gap-4'>
|
||||
<p className='text-sm text-zinc-300 text-center sm:text-right'>
|
||||
{data.length} of {allocationLimit} allowed allocations
|
||||
</p>
|
||||
{allocationLimit > data.length && (
|
||||
@@ -70,7 +72,7 @@ const NetworkContainer = () => {
|
||||
background:
|
||||
'radial-gradient(124.75% 124.75% at 50.01% -10.55%, rgb(36, 36, 36) 0%, rgb(20, 20, 20) 100%)',
|
||||
}}
|
||||
className='px-8 py-3 border-[1px] border-[#ffffff12] rounded-full text-sm font-bold shadow-md cursor-pointer'
|
||||
className='px-8 py-3 border-[1px] border-[#ffffff12] rounded-full text-sm font-bold shadow-md cursor-pointer hover:bg-[#ffffff11] transition-colors duration-150'
|
||||
onClick={onCreateAllocation}
|
||||
>
|
||||
New Allocation
|
||||
@@ -82,16 +84,41 @@ const NetworkContainer = () => {
|
||||
</>
|
||||
)}
|
||||
</MainPageHeader>
|
||||
{!data ? null : (
|
||||
<>
|
||||
<PageListContainer data-pyro-network-container-allocations>
|
||||
<For each={data} memo>
|
||||
{(allocation) => (
|
||||
<AllocationRow key={`${allocation.ip}:${allocation.port}`} allocation={allocation} />
|
||||
)}
|
||||
</For>
|
||||
</PageListContainer>
|
||||
</>
|
||||
|
||||
{!data ? (
|
||||
<div className='flex items-center justify-center py-12'>
|
||||
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-brand'></div>
|
||||
</div>
|
||||
) : data.length > 0 ? (
|
||||
<PageListContainer data-pyro-network-container-allocations>
|
||||
<For each={data} memo>
|
||||
{(allocation) => (
|
||||
<AllocationRow key={`${allocation.ip}:${allocation.port}`} allocation={allocation} />
|
||||
)}
|
||||
</For>
|
||||
</PageListContainer>
|
||||
) : (
|
||||
<div className='flex flex-col items-center justify-center py-12 px-4'>
|
||||
<div className='text-center'>
|
||||
<div className='w-16 h-16 mx-auto mb-4 rounded-full bg-[#ffffff11] flex items-center justify-center'>
|
||||
<svg className='w-8 h-8 text-zinc-400' fill='currentColor' viewBox='0 0 20 20'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className='text-lg font-medium text-zinc-200 mb-2'>
|
||||
{allocationLimit > 0 ? 'No allocations found' : 'Allocations unavailable'}
|
||||
</h3>
|
||||
<p className='text-sm text-zinc-400 max-w-sm'>
|
||||
{allocationLimit > 0
|
||||
? 'Your server does not have any network allocations. Create one to get started.'
|
||||
: 'Network allocations cannot be created for this server.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ServerContentBlock>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { on } from 'events';
|
||||
import type React from 'react';
|
||||
import { Fragment, Suspense, useEffect, useRef, useState } from 'react';
|
||||
import React, { Fragment, Suspense, useEffect, useRef, useState } from 'react';
|
||||
import { NavLink, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import routes from '@/routers/routes';
|
||||
@@ -48,6 +47,84 @@ import { ServerContext } from '@/state/server';
|
||||
|
||||
const blank_egg_prefix = '@';
|
||||
|
||||
// Sidebar item components that check both permissions and feature limits
|
||||
const DatabasesSidebarItem = React.forwardRef<HTMLAnchorElement, { id: string; onClick: () => void }>(
|
||||
({ id, onClick }, ref) => {
|
||||
const databaseLimit = ServerContext.useStoreState((state) => state.server.data?.featureLimits.databases ?? 0);
|
||||
|
||||
// Hide if no database access (limit is 0)
|
||||
if (databaseLimit === 0) return null;
|
||||
|
||||
return (
|
||||
<Can action={'database.*'} matchAny>
|
||||
<NavLink
|
||||
className='flex flex-row items-center transition-colors duration-200 hover:bg-[#ffffff11] rounded-md'
|
||||
ref={ref}
|
||||
to={`/server/${id}/databases`}
|
||||
onClick={onClick}
|
||||
end
|
||||
>
|
||||
<HugeIconsDatabase fill='currentColor' />
|
||||
<p>Databases</p>
|
||||
</NavLink>
|
||||
</Can>
|
||||
);
|
||||
},
|
||||
);
|
||||
DatabasesSidebarItem.displayName = 'DatabasesSidebarItem';
|
||||
|
||||
const BackupsSidebarItem = React.forwardRef<HTMLAnchorElement, { id: string; onClick: () => void }>(
|
||||
({ id, onClick }, ref) => {
|
||||
const backupLimit = ServerContext.useStoreState((state) => state.server.data?.featureLimits.backups ?? 0);
|
||||
|
||||
// Hide if no backup access (limit is 0)
|
||||
if (backupLimit === 0) return null;
|
||||
|
||||
return (
|
||||
<Can action={'backup.*'} matchAny>
|
||||
<NavLink
|
||||
className='flex flex-row items-center transition-colors duration-200 hover:bg-[#ffffff11] rounded-md'
|
||||
ref={ref}
|
||||
to={`/server/${id}/backups`}
|
||||
onClick={onClick}
|
||||
end
|
||||
>
|
||||
<HugeIconsCloudUp fill='currentColor' />
|
||||
<p>Backups</p>
|
||||
</NavLink>
|
||||
</Can>
|
||||
);
|
||||
},
|
||||
);
|
||||
BackupsSidebarItem.displayName = 'BackupsSidebarItem';
|
||||
|
||||
const NetworkingSidebarItem = React.forwardRef<HTMLAnchorElement, { id: string; onClick: () => void }>(
|
||||
({ id, onClick }, ref) => {
|
||||
const allocationLimit = ServerContext.useStoreState(
|
||||
(state) => state.server.data?.featureLimits.allocations ?? 0,
|
||||
);
|
||||
|
||||
// Hide if no allocation access (limit is 0)
|
||||
if (allocationLimit === 0) return null;
|
||||
|
||||
return (
|
||||
<Can action={'allocation.*'} matchAny>
|
||||
<NavLink
|
||||
className='flex flex-row items-center transition-colors duration-200 hover:bg-[#ffffff11] rounded-md'
|
||||
ref={ref}
|
||||
to={`/server/${id}/network`}
|
||||
onClick={onClick}
|
||||
end
|
||||
>
|
||||
<HugeIconsConnections fill='currentColor' />
|
||||
<p>Networking</p>
|
||||
</NavLink>
|
||||
</Can>
|
||||
);
|
||||
},
|
||||
);
|
||||
NetworkingSidebarItem.displayName = 'NetworkingSidebarItem';
|
||||
|
||||
interface Egg {
|
||||
object: string;
|
||||
attributes: {
|
||||
@@ -453,42 +530,17 @@ const ServerRouter = () => {
|
||||
<p>Files</p>
|
||||
</NavLink>
|
||||
</Can>
|
||||
<Can action={'database.*'} matchAny>
|
||||
<NavLink
|
||||
className='flex flex-row items-center transition-colors duration-200 hover:bg-[#ffffff11] rounded-md'
|
||||
ref={NavigationDatabases}
|
||||
to={`/server/${id}/databases`}
|
||||
onClick={toggleSidebar}
|
||||
end
|
||||
>
|
||||
<HugeIconsDatabase fill='currentColor' />
|
||||
<p>Databases</p>
|
||||
</NavLink>
|
||||
</Can>
|
||||
<Can action={'backup.*'} matchAny>
|
||||
<NavLink
|
||||
className='flex flex-row items-center transition-colors duration-200 hover:bg-[#ffffff11] rounded-md'
|
||||
ref={NavigationBackups}
|
||||
to={`/server/${id}/backups`}
|
||||
onClick={toggleSidebar}
|
||||
end
|
||||
>
|
||||
<HugeIconsCloudUp fill='currentColor' />
|
||||
<p>Backups</p>
|
||||
</NavLink>
|
||||
</Can>
|
||||
<Can action={'allocation.*'} matchAny>
|
||||
<NavLink
|
||||
className='flex flex-row items-center transition-colors duration-200 hover:bg-[#ffffff11] rounded-md'
|
||||
ref={NavigationNetworking}
|
||||
to={`/server/${id}/network`}
|
||||
onClick={toggleSidebar}
|
||||
end
|
||||
>
|
||||
<HugeIconsConnections fill='currentColor' />
|
||||
<p>Networking</p>
|
||||
</NavLink>
|
||||
</Can>
|
||||
<DatabasesSidebarItem
|
||||
id={id}
|
||||
ref={NavigationDatabases}
|
||||
onClick={toggleSidebar}
|
||||
/>
|
||||
<BackupsSidebarItem id={id} ref={NavigationBackups} onClick={toggleSidebar} />
|
||||
<NetworkingSidebarItem
|
||||
id={id}
|
||||
ref={NavigationNetworking}
|
||||
onClick={toggleSidebar}
|
||||
/>
|
||||
<Can action={'user.*'} matchAny>
|
||||
<NavLink
|
||||
className='flex flex-row items-center transition-colors duration-200 hover:bg-[#ffffff11] rounded-md'
|
||||
|
||||
Reference in New Issue
Block a user