Merge branch 'develop' into feature/bulk-reinstall-command
This commit is contained in:
commit
215351eeb3
308 changed files with 18740 additions and 3400 deletions
|
@ -1,22 +1,27 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'not_authorized' => 'You are not authorized to perform this action.',
|
||||
'auth_error' => 'There was an error while attempting to login.',
|
||||
'authentication_required' => 'Authentication is required to continue.',
|
||||
'remember_me' => 'Remember Me',
|
||||
'sign_in' => 'Sign In',
|
||||
'forgot_password' => 'I\'ve forgotten my password!',
|
||||
'request_reset_text' => 'Forgotten your account password? It is not the end of the world, just provide your email below.',
|
||||
'reset_password_text' => 'Reset your account password.',
|
||||
'reset_password' => 'Reset Account Password',
|
||||
'email_sent' => 'An email has been sent to you with further instructions for resetting your password.',
|
||||
'failed' => 'The credentials provided do not match those we have on record, or the 2FA token provided was invalid.',
|
||||
'go_to_login' => 'Go to Login',
|
||||
'failed' => 'No account matching those credentials could be found.',
|
||||
|
||||
'forgot_password' => [
|
||||
'label' => 'Forgot Password?',
|
||||
'label_help' => 'Enter your account email address to receive instructions on resetting your password.',
|
||||
'button' => 'Recover Account',
|
||||
],
|
||||
|
||||
'reset_password' => [
|
||||
'button' => 'Reset and Sign In',
|
||||
],
|
||||
|
||||
'two_factor' => [
|
||||
'label' => '2-Factor Token',
|
||||
'label_help' => 'This account requires a second layer of authentication in order to continue. Please enter the code generated by your device to complete this login.',
|
||||
'checkpoint_failed' => 'The two-factor authentication token was invalid.',
|
||||
],
|
||||
|
||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||
'password_requirements' => 'Passwords must contain at least one uppercase, lowercase, and numeric character and must be at least 8 characters in length.',
|
||||
'request_reset' => 'Locate Account',
|
||||
'2fa_required' => '2-Factor Authentication',
|
||||
'2fa_failed' => 'The 2FA token provided was invalid.',
|
||||
'totp_failed' => 'There was an error while attempting to validate TOTP.',
|
||||
'password_requirements' => 'Password must be at least 8 characters in length and should be unique to this site.',
|
||||
'2fa_must_be_enabled' => 'The administrator has required that 2-Factor Authentication be enabled for your account in order to use the Panel.',
|
||||
];
|
||||
|
|
|
@ -54,36 +54,4 @@ return [
|
|||
],
|
||||
],
|
||||
],
|
||||
'account' => [
|
||||
'details_updated' => 'Your account details have been successfully updated.',
|
||||
'invalid_password' => 'The password provided for your account was not valid.',
|
||||
'header' => 'Your Account',
|
||||
'header_sub' => 'Manage your account details.',
|
||||
'update_pass' => 'Update Password',
|
||||
'update_email' => 'Update Email Address',
|
||||
'current_password' => 'Current Password',
|
||||
'new_password' => 'New Password',
|
||||
'new_password_again' => 'Repeat New Password',
|
||||
'new_email' => 'New Email Address',
|
||||
'first_name' => 'First Name',
|
||||
'last_name' => 'Last Name',
|
||||
'update_identity' => 'Update Identity',
|
||||
'username_help' => 'Your username must be unique to your account, and may only contain the following characters: :requirements.',
|
||||
'language' => 'Language',
|
||||
],
|
||||
'security' => [
|
||||
'session_mgmt_disabled' => 'Your host has not enabled the ability to manage account sessions via this interface.',
|
||||
'header' => 'Account Security',
|
||||
'header_sub' => 'Control active sessions and 2-Factor Authentication.',
|
||||
'sessions' => 'Active Sessions',
|
||||
'2fa_header' => '2-Factor Authentication',
|
||||
'2fa_token_help' => 'Enter the 2FA Token generated by your app (Google Authenticator, Authy, etc.).',
|
||||
'disable_2fa' => 'Disable 2-Factor Authentication',
|
||||
'2fa_enabled' => '2-Factor Authentication is enabled on this account and will be required in order to login to the panel. If you would like to disable 2FA, simply enter a valid token below and submit the form.',
|
||||
'2fa_disabled' => '2-Factor Authentication is disabled on your account! You should enable 2FA in order to add an extra level of protection on your account.',
|
||||
'enable_2fa' => 'Enable 2-Factor Authentication',
|
||||
'2fa_qr' => 'Configure 2FA on Your Device',
|
||||
'2fa_checkpoint_help' => 'Use the 2FA application on your phone to take a picture of the QR code to the left, or manually enter the code under it. Once you have done so, generate a token and enter it below.',
|
||||
'2fa_disable_error' => 'The 2FA token provided was not valid. Protection has not been disabled for this account.',
|
||||
],
|
||||
];
|
||||
|
|
28
resources/lang/en/dashboard/account.php
Normal file
28
resources/lang/en/dashboard/account.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'email' => [
|
||||
'title' => 'Update your email',
|
||||
'updated' => 'Your email address has been updated.',
|
||||
],
|
||||
'password' => [
|
||||
'title' => 'Change your password',
|
||||
'requirements' => 'Your new password should be at least 8 characters in length.',
|
||||
'updated' => 'Your password has been updated.',
|
||||
],
|
||||
'two_factor' => [
|
||||
'button' => 'Configure 2-Factor Authentication',
|
||||
'disabled' => 'Two-factor authentication has been disabled on your account. You will no longer be prompted to provide a token when logging in.',
|
||||
'enabled' => 'Two-factor authentication has been enabled on your account! From now on, when logging in, you will be required to provide the code generated by your device.',
|
||||
'invalid' => 'The token provided was invalid.',
|
||||
'setup' => [
|
||||
'title' => 'Setup two-factor authentication',
|
||||
'help' => 'Can\'t scan the code? Enter the code below into your application:',
|
||||
'field' => 'Enter token',
|
||||
],
|
||||
'disable' => [
|
||||
'title' => 'Disable two-factor authentication',
|
||||
'field' => 'Enter token',
|
||||
],
|
||||
],
|
||||
];
|
8
resources/lang/en/dashboard/index.php
Normal file
8
resources/lang/en/dashboard/index.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'search' => 'Search for servers...',
|
||||
'no_matches' => 'There were no servers found matching the search criteria provided.',
|
||||
'cpu_title' => 'CPU',
|
||||
'memory_title' => 'Memory',
|
||||
];
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
return [
|
||||
'email' => 'Email',
|
||||
'email_address' => 'Email address',
|
||||
'user_identifier' => 'Username or Email',
|
||||
'password' => 'Password',
|
||||
'confirm_password' => 'Confirm Password',
|
||||
'new_password' => 'New password',
|
||||
'confirm_password' => 'Confirm new password',
|
||||
'login' => 'Login',
|
||||
'home' => 'Home',
|
||||
'servers' => 'Servers',
|
||||
|
@ -85,4 +87,8 @@ return [
|
|||
'sat' => 'Saturday',
|
||||
],
|
||||
'last_used' => 'Last Used',
|
||||
'enable' => 'Enable',
|
||||
'disable' => 'Disable',
|
||||
'save' => 'Save',
|
||||
'copyright' => '© 2015 - :year Pterodactyl Software',
|
||||
];
|
||||
|
|
|
@ -101,5 +101,6 @@ return [
|
|||
// Internal validation logic for Pterodactyl
|
||||
'internal' => [
|
||||
'variable_value' => ':env variable',
|
||||
'invalid_password' => 'The password provided was invalid for this account.',
|
||||
],
|
||||
];
|
||||
|
|
33
resources/lang/ru/admin/nests.php
Normal file
33
resources/lang/ru/admin/nests.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
/**
|
||||
* Pterodactyl - Panel
|
||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||
*
|
||||
* This software is licensed under the terms of the MIT license.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
return [
|
||||
'notices' => [
|
||||
'created' => 'Новое гнездо :name успешно создано.',
|
||||
'deleted' => 'Успешно удалили запрошенное гнездо с панели.',
|
||||
'updated' => 'Успешно обновлены параметры конфигурации гнезда.',
|
||||
],
|
||||
'eggs' => [
|
||||
'notices' => [
|
||||
'imported' => 'Успешно импортировано это яйцо и связанные с ним переменные.',
|
||||
'updated_via_import' => 'Это яйцо было обновлено с использованием предоставленного файла.',
|
||||
'deleted' => 'Успешно удалили запрошенное яйцо из панели.',
|
||||
'updated' => 'Конфигурация яйца успешно обновлена.',
|
||||
'script_updated' => 'Сценарий установки Egg обновлен и будет запускаться всякий раз, когда будут установлены серверы.',
|
||||
'egg_created' => 'Новое яйцо было добавлено успешно. Вам нужно будет перезапустить все работающие демоны, чтобы применить это новое яйцо.',
|
||||
],
|
||||
],
|
||||
'variables' => [
|
||||
'notices' => [
|
||||
'variable_deleted' => 'Переменная ":variable" была удалена и больше не будет доступна серверам после восстановления.',
|
||||
'variable_updated' => 'Переменная ":variable" была обновлена. Вам нужно будет перестроить все серверы, использующие эту переменную, чтобы применить изменения.',
|
||||
'variable_created' => 'Новая переменная была успешно создана и назначена этому яйцу.',
|
||||
],
|
||||
],
|
||||
];
|
23
resources/lang/ru/admin/node.php
Normal file
23
resources/lang/ru/admin/node.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
/**
|
||||
* Pterodactyl - Panel
|
||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||
*
|
||||
* This software is licensed under the terms of the MIT license.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
return [
|
||||
'validation' => [
|
||||
'fqdn_not_resolvable' => 'Указанное полное доменное имя или IP-адрес не преобразуется в действительный IP-адрес.',
|
||||
'fqdn_required_for_ssl' => 'Для использования SSL для этого узла требуется полное доменное имя, которое разрешается в публичный IP-адрес.',
|
||||
],
|
||||
'notices' => [
|
||||
'allocations_added' => 'Локации успешно добавлены в этот узел.',
|
||||
'node_deleted' => 'Узел успешно удален с панели.',
|
||||
'location_required' => 'У вас должно быть настроено хотя бы одно местоположение, прежде чем вы сможете добавить узел на эту панель.',
|
||||
'node_created' => 'Успешно создан новый узел. Вы можете автоматически настроить демон на этом компьютере, посетив вкладку \'Configuration\'. <strong> Прежде чем вы сможете добавить какие-либо серверы, вы должны сначала выделить как минимум один IP-адрес и порт.</strong>',
|
||||
'node_updated' => 'Информация об узле обновлена. Если какие-либо настройки демона были изменены, вам нужно будет перезагрузить их, чтобы эти изменения вступили в силу.',
|
||||
'unallocated_deleted' => 'Удалил все нераспределенные порты для <code>:ip</code>.',
|
||||
],
|
||||
];
|
16
resources/lang/ru/admin/pack.php
Normal file
16
resources/lang/ru/admin/pack.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* Pterodactyl - Panel
|
||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||
*
|
||||
* This software is licensed under the terms of the MIT license.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
return [
|
||||
'notices' => [
|
||||
'pack_updated' => 'Пак успешно обновлен.',
|
||||
'pack_deleted' => 'Успешно удален пакет ":name" из системы.',
|
||||
'pack_created' => 'Новый пакет был успешно создан в системе и теперь доступен для развертывания на серверах.',
|
||||
],
|
||||
];
|
31
resources/lang/ru/admin/server.php
Normal file
31
resources/lang/ru/admin/server.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/**
|
||||
* Pterodactyl - Panel
|
||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||
*
|
||||
* This software is licensed under the terms of the MIT license.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
return [
|
||||
'exceptions' => [
|
||||
'no_new_default_allocation' => 'Вы пытаетесь удалить выделение по умолчанию для этого сервера, но резервное выделение для использования отсутствует.',
|
||||
'marked_as_failed' => 'Этот сервер был помечен как Проблемный предыдущей установки. Текущий статус не может быть переключен в этом состоянии.',
|
||||
'bad_variable' => 'Произошла ошибка проверки с переменной :name.',
|
||||
'daemon_exception' => 'При попытке установить связь с демоном возникла исключительная ситуация, в результате которой был получен код ответа HTTP / :code. Это исключение было зарегистрировано.',
|
||||
'default_allocation_not_found' => 'Запрошенное распределение по умолчанию не было найдено в выделениях этого сервера.',
|
||||
],
|
||||
'alerts' => [
|
||||
'startup_changed' => 'Начальная конфигурация для этого сервера была обновлена. Если гнездо или яйцо этого сервера было изменено, то теперь будет происходить переустановка.',
|
||||
'server_deleted' => 'Сервер успешно удален из системы.',
|
||||
'server_created' => 'Сервер буспешно создан из панели. Пожалуйста, дайте демону несколько минут, чтобы полностью установить этот сервер.',
|
||||
'build_updated' => 'Детали сборки для этого сервера были обновлены. Некоторые изменения могут потребовать перезагрузки для вступления в силу.',
|
||||
'suspension_toggled' => 'Состояние приостановки сервера изменено на :status.',
|
||||
'rebuild_on_boot' => 'Этот сервер был помечен как требующий перестройки Docker Container. Это произойдет при следующем запуске сервера.',
|
||||
'install_toggled' => 'Состояние установки для этого сервера переключено.',
|
||||
'server_reinstalled' => 'Этот сервер поставлен в очередь для начала переустановки.',
|
||||
'details_updated' => 'Данные сервера успешно обновлены.',
|
||||
'docker_image_updated' => 'Успешно изменен образ Docker по умолчанию для использования на этом сервере. Чтобы применить это изменение, требуется перезагрузка.',
|
||||
'node_required' => 'У вас должен быть настроен хотя бы один узел, прежде чем вы сможете добавить сервер на эту панель.',
|
||||
],
|
||||
];
|
18
resources/lang/ru/admin/user.php
Normal file
18
resources/lang/ru/admin/user.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
/**
|
||||
* Pterodactyl - Panel
|
||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
||||
*
|
||||
* This software is licensed under the terms of the MIT license.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
return [
|
||||
'exceptions' => [
|
||||
'user_has_servers' => 'Невозможно удалить пользователя с активными серверами, подключенными к его учетной записи. Пожалуйста, удалите их серверы, прежде чем продолжить.',
|
||||
],
|
||||
'notices' => [
|
||||
'account_created' => 'Аккаунт успешно создан.',
|
||||
'account_updated' => 'Аккаунт успешно обновлен.',
|
||||
],
|
||||
];
|
22
resources/lang/ru/auth.php
Normal file
22
resources/lang/ru/auth.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'not_authorized' => 'Вы не авторизованы для выполнения этого действия.',
|
||||
'auth_error' => 'Произошла ошибка при попытке войти.',
|
||||
'authentication_required' => 'Для продолжения требуется Аутентификация.',
|
||||
'remember_me' => 'Запомнить меня',
|
||||
'sign_in' => 'Войти в систему',
|
||||
'forgot_password' => 'Я забыл свой пароль!',
|
||||
'request_reset_text' => 'Забыли пароль учетной записи? Это не конец света, просто укажите свой адрес электронной почты.',
|
||||
'reset_password_text' => 'Сбросить пароль вашей учетной записи.',
|
||||
'reset_password' => 'Сбросить пароль учетной записи',
|
||||
'email_sent' => 'Вам отправлено электронное письмо с дальнейшими инструкциями по восстановлению пароля.',
|
||||
'failed' => 'Предоставленные учетные данные не совпадают с теми, которые есть у нас в записи, или предоставленный токен 2FA недействителен.',
|
||||
'throttle' => 'Слишком много попыток входа в систему. Пожалуйста, повторите попытку через :seconds секунд.',
|
||||
'password_requirements' => 'Пароли должны содержать как минимум одну заглавную, строчную букву, цифры и содержать не менее 8 символов.',
|
||||
'request_reset' => 'Найти аккаунт',
|
||||
'2fa_required' => 'Двухфакторная аутентификация',
|
||||
'2fa_failed' => 'Указанный токен 2FA недействителен.',
|
||||
'totp_failed' => 'Произошла ошибка при попытке проверить TOTP.',
|
||||
'2fa_must_be_enabled' => 'Администратор потребовал, чтобы для вашей учетной записи была включена двухфакторная аутентификация, чтобы использовать панель.',
|
||||
];
|
89
resources/lang/ru/base.php
Normal file
89
resources/lang/ru/base.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'validation_error' => 'Произошла ошибка с одним или несколькими полями в запросе.',
|
||||
'errors' => [
|
||||
'return' => 'Вернуться на предыдущую страницу',
|
||||
'home' => 'Домой',
|
||||
'403' => [
|
||||
'header' => 'Запрещено',
|
||||
'desc' => 'У вас нет прав доступа к ресурсам на этом сервере.',
|
||||
],
|
||||
'404' => [
|
||||
'header' => 'Файл не найден',
|
||||
'desc' => 'Нам не удалось найти запрошенный ресурс на сервере.',
|
||||
],
|
||||
'installing' => [
|
||||
'header' => 'Установка сервера',
|
||||
'desc' => 'Запрошенный сервер все еще завершает процесс установки. Пожалуйста, зайдите через несколько минут, вы должны получить электронное письмо, как только этот процесс будет завершен.',
|
||||
],
|
||||
'suspended' => [
|
||||
'header' => 'Сервер приостановлен',
|
||||
'desc' => 'Этот сервер был приостановлен и недоступен.',
|
||||
],
|
||||
'maintenance' => [
|
||||
'header' => 'Узел в обслуживании',
|
||||
'title' => 'Временно недоступен',
|
||||
'desc' => 'Этот узел находится на обслуживании, поэтому ваш сервер может быть временно недоступен.',
|
||||
],
|
||||
],
|
||||
'index' => [
|
||||
'header' => 'Ваши серверы',
|
||||
'header_sub' => 'Серверы, к которым у вас есть доступ.',
|
||||
'list' => 'Список серверов',
|
||||
],
|
||||
'api' => [
|
||||
'index' => [
|
||||
'list' => 'Ваши ключи',
|
||||
'header' => 'API аккаунта',
|
||||
'header_sub' => 'Управляйте клавишами доступа, которые позволяют вам выполнять действия с панелью.',
|
||||
'create_new' => 'Создать новый ключ API',
|
||||
'keypair_created' => 'Ключ API был успешно сгенерирован и показан ниже.',
|
||||
],
|
||||
'new' => [
|
||||
'header' => 'овый ключ API',
|
||||
'header_sub' => 'Создайте новый ключ доступа к учетной записи.',
|
||||
'form_title' => 'Подробности',
|
||||
'descriptive_memo' => [
|
||||
'title' => 'Описание',
|
||||
'description' => 'Введите краткое описание этого ключа, которое будет полезно для справки.',
|
||||
],
|
||||
'allowed_ips' => [
|
||||
'title' => 'Разрешенные IP-адреса',
|
||||
'description' => 'Введите разделенный строкой список IP-адресов, которым разрешен доступ к API с помощью этого ключа. Нотация CIDR разрешена. Оставьте пустым, чтобы разрешить любой IP.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'account' => [
|
||||
'details_updated' => 'Данные вашей учетной записи успешно обновлены.',
|
||||
'invalid_password' => 'Пароль для вашей учетной записи недействителен.',
|
||||
'header' => 'Ваш аккаунт',
|
||||
'header_sub' => 'Управляйте данными своей учетной записи.',
|
||||
'update_pass' => 'Обновить пароль',
|
||||
'update_email' => 'Обновить адрес электронной почты',
|
||||
'current_password' => 'Текущий пароль',
|
||||
'new_password' => 'Новый пароль',
|
||||
'new_password_again' => 'Повторите новый пароль',
|
||||
'new_email' => 'Новый E-mail адрес',
|
||||
'first_name' => 'Имя',
|
||||
'last_name' => 'Фамилия',
|
||||
'update_identity' => 'Обновить данные',
|
||||
'username_help' => 'Ваше имя пользователя должно быть уникальным для вашей учетной записи и может содержать только следующие символы: :requirements.',
|
||||
'language' => 'Язык',
|
||||
],
|
||||
'security' => [
|
||||
'session_mgmt_disabled' => 'Ваш хост не включил возможность управлять сеансами аккаунта через этот интерфейс.',
|
||||
'header' => 'Безопасность аккаунта',
|
||||
'header_sub' => 'Контроль активных сессий и 2-х факторной аутентификации.',
|
||||
'sessions' => 'Активная сессия',
|
||||
'2fa_header' => '2-х факторная аутентификация',
|
||||
'2fa_token_help' => 'Введите токен 2FA, созданный вашим приложением (Google Authenticator, Authy, и так далее.).',
|
||||
'disable_2fa' => 'Отключить двухфакторную аутентификацию',
|
||||
'2fa_enabled' => 'В этой учетной записи включена двухфакторная аутентификация, которая потребуется для входа в панель. Если вы хотите отключить 2FA, просто введите действительный токен ниже и отправьте форму.',
|
||||
'2fa_disabled' => 'Двухфакторная аутентификация отключена на вашем аккаунте! Вы должны включить 2FA, чтобы добавить дополнительный уровень защиты для вашей учетной записи.',
|
||||
'enable_2fa' => 'Включить двухфакторную аутентификацию',
|
||||
'2fa_qr' => 'Настройте 2FA на вашем устройстве',
|
||||
'2fa_checkpoint_help' => 'Используйте приложение 2FA на своем телефоне, чтобы сделать снимок QR-кода, или вручную введите код под ним. После этого сгенерируйте токен и введите его ниже.',
|
||||
'2fa_disable_error' => 'Указанный токен 2FA недействителен. Защита не была отключена для этой учетной записи.',
|
||||
],
|
||||
];
|
97
resources/lang/ru/command/messages.php
Normal file
97
resources/lang/ru/command/messages.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'key' => [
|
||||
'warning' => 'Похоже, вы уже настроили ключ шифрования приложения. Продолжая этот процесс с перезаписать этот ключ и вызвать повреждение данных для любых существующих зашифрованных данных. НЕ ПРОДОЛЖАЙТЕ, ЕСЛИ ВЫ НЕ ЗНАЕТЕ, ЧТО ВЫ ДЕЛАЕТЕ.',
|
||||
'confirm' => 'Я понимаю последствия выполнения этой команды и принимаю на себя всю ответственность за потерю зашифрованных данных.',
|
||||
'final_confirm' => 'Вы уверены, что хотите продолжить? Изменение ключа шифрования приложения приведет к потере данных.',
|
||||
],
|
||||
'location' => [
|
||||
'no_location_found' => 'Не удалось найти запись, соответствующую предоставленному короткому коду.',
|
||||
'ask_short' => 'Короткий код местоположения',
|
||||
'ask_long' => 'Описание местоположения',
|
||||
'created' => 'Успешно создано новое местоположение (:name) с идентификатором :id.',
|
||||
'deleted' => 'Успешно удалено запрошенное местоположение.',
|
||||
],
|
||||
'user' => [
|
||||
'search_users' => 'Введите имя пользователя, UUID или адрес электронной почты',
|
||||
'select_search_user' => 'ID пользователя для удаления (Enter \'0\' to re-search)',
|
||||
'deleted' => 'Пользователь успешно удален из Панели.',
|
||||
'confirm_delete' => 'Вы уверены, что хотите удалить этого пользователя с панели?',
|
||||
'no_users_found' => 'По данному запросу не найдено ни одного пользователя.',
|
||||
'multiple_found' => 'Для предоставленного пользователя было найдено несколько учетных записей, не удалось удалить пользователя из-за --no-interaction flag.',
|
||||
'ask_admin' => 'Является ли этот пользователь администратором?',
|
||||
'ask_email' => 'Адрес электронной почты',
|
||||
'ask_username' => 'Ник пользователя',
|
||||
'ask_name_first' => 'Имя',
|
||||
'ask_name_last' => 'Фамилия',
|
||||
'ask_password' => 'Пароль',
|
||||
'ask_password_tip' => 'Если вы хотите создать учетную запись со случайным паролем, отправленным пользователю по электронной почте, повторите команду (CTRL + C) и передайте `--no-password` flag.',
|
||||
'ask_password_help' => 'Пароли должны быть длиной не менее 8 символов и содержать как минимум одну заглавную букву и цифру.',
|
||||
'2fa_help_text' => [
|
||||
'Эта команда отключит двухфакторную аутентификацию для учетной записи пользователя, если она включена. Это следует использовать только в качестве команды восстановления учетной записи, если пользователь заблокирован в своей учетной записи.',
|
||||
'Если это не то, что вы хотели сделать, нажмите CTRL + C, чтобы выйти из этого процесса.',
|
||||
],
|
||||
'2fa_disabled' => 'Двухфакторная аутентификация была отключена для :email.',
|
||||
],
|
||||
'schedule' => [
|
||||
'output_line' => 'Диспетчерская работа для первой задачи в `:schedule` (:hash).',
|
||||
],
|
||||
'maintenance' => [
|
||||
'deleting_service_backup' => 'Удаление файла резервной копии службы :file.',
|
||||
],
|
||||
'server' => [
|
||||
'rebuild_failed' => 'Запрос на перестроение для ":name" (#:id) на узле ":node" завершился ошибкой: :message',
|
||||
'power' => [
|
||||
'confirm' => 'Вы собираетесь выполнить :action против :count серверов. Вы хотите продолжить?',
|
||||
'action_failed' => 'Запрос на действие питания для ":name" (#:id) на узле ":node" завершился ошибкой: :message',
|
||||
],
|
||||
],
|
||||
'environment' => [
|
||||
'mail' => [
|
||||
'ask_smtp_host' => 'SMTP Хост (e.g. smtp.gmail.com)',
|
||||
'ask_smtp_port' => 'SMTP Порт',
|
||||
'ask_smtp_username' => 'SMTP Пользователь',
|
||||
'ask_smtp_password' => 'SMTP Password',
|
||||
'ask_mailgun_domain' => 'Почтовый Домен',
|
||||
'ask_mailgun_secret' => 'Mailgun Secret',
|
||||
'ask_mandrill_secret' => 'Mandrill Secret',
|
||||
'ask_postmark_username' => 'Postmark API Key',
|
||||
'ask_driver' => 'Какой драйвер следует использовать для отправки писем?',
|
||||
'ask_mail_from' => 'Адреса электронной почты должны исходить от',
|
||||
'ask_mail_name' => 'Имя, с которого должны начинаться электронные письма',
|
||||
'ask_encryption' => 'Метод шифрования для использования',
|
||||
],
|
||||
'database' => [
|
||||
'host_warning' => 'Настоятельно рекомендуется не использовать "localhost" в качестве хоста базы данных, поскольку мы часто сталкиваемся с проблемами подключения к сокету. Если вы хотите использовать локальное соединение, вы должны использовать "127.0.0.1".',
|
||||
'host' => 'Хост базы данных',
|
||||
'port' => 'Порт базы данных',
|
||||
'database' => 'Название базы данных',
|
||||
'username_warning' => 'Использование учетной записи "root" для подключений MySQL не только не одобряется, но и не допускается этим приложением. Вам нужно создать пользователя MySQL для этого программного обеспечения.',
|
||||
'username' => 'Имя пользователя базы данных',
|
||||
'password_defined' => 'Похоже, вы уже определили пароль для подключения к MySQL, хотите изменить его?',
|
||||
'password' => 'Пароль базы данных',
|
||||
'connection_error' => 'Невозможно подключиться к серверу MySQL с использованием предоставленных учетных данных. Возвращенная ошибка ":error".',
|
||||
'creds_not_saved' => 'Ваши учетные данные НЕ были сохранены. Вам нужно будет предоставить действительную информацию о соединении, прежде чем продолжить.',
|
||||
'try_again' => 'Вернуться и попробуйте снова?',
|
||||
],
|
||||
'app' => [
|
||||
'settings' => 'Включить редактор настроек на основе интерфейса?',
|
||||
'author' => 'E-mail автора яйца',
|
||||
'author_help' => 'Укажите адрес электронной почты, с которого должны быть отправлены яйца, экспортируемые этой панелью. Это должен быть действительный адрес электронной почты.',
|
||||
'app_url_help' => 'URL приложения ДОЛЖЕН начинаться с https:// или http:// в зависимости от того, используете ли вы SSL или нет. Если вы не включите схему, ваши электронные письма и другой контент будут ссылаться на неправильное местоположение.',
|
||||
'app_url' => 'URL приложения',
|
||||
'timezone_help' => 'Часовой пояс должен соответствовать одному из поддерживаемых часовых поясов PHP. Если вы не уверены, пожалуйста перейдите по ссылке http://php.net/manual/en/timezones.php.',
|
||||
'timezone' => 'Часовой пояс приложения',
|
||||
'cache_driver' => 'Драйвер кеша',
|
||||
'session_driver' => 'Драйвер сеанса',
|
||||
'queue_driver' => 'Драйвер очереди ',
|
||||
'using_redis' => 'Вы выбрали драйвер Redis для одного или нескольких параметров. Пожалуйста, предоставьте действительную информацию о подключении ниже. В большинстве случаев вы можете использовать предоставленные значения по умолчанию, если только вы не изменили настройки.',
|
||||
'redis_host' => 'Ност Redis',
|
||||
'redis_password' => 'Пароль Redis',
|
||||
'redis_pass_help' => 'По умолчанию экземпляр сервера Redis не имеет пароля, поскольку он работает локально и недоступен для внешнего мира. Если это так, просто нажмите Enter, не вводя значение.',
|
||||
'redis_port' => 'Порт Redis',
|
||||
'redis_pass_defined' => 'Кажется, пароль для Redis уже определен, вы хотите его изменить?',
|
||||
],
|
||||
],
|
||||
];
|
68
resources/lang/ru/exceptions.php
Normal file
68
resources/lang/ru/exceptions.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'daemon_connection_failed' => 'При попытке установить связь с демоном возникла исключительная ситуация, в результате которой был получен код ответа HTTP /:code. Это исключение было зарегистрировано.',
|
||||
'node' => [
|
||||
'servers_attached' => 'Узел не должен иметь серверов, связанных с ним, для удаления.',
|
||||
'daemon_off_config_updated' => 'Конфигурация демона <strong>была обновлена</strong>, однако при попытке автоматического обновления файла конфигурации в демоне произошла ошибка. Вам нужно будет вручную обновить файл конфигурации (core.json), чтобы демон применил эти изменения.',
|
||||
],
|
||||
'allocations' => [
|
||||
'server_using' => 'Сервер в настоящее время назначен на это распределение. Распределение может быть удалено, только если в данный момент не назначен сервер.',
|
||||
'too_many_ports' => 'Добавление более 1000 портов в одном диапазоне одновременно не поддерживается.',
|
||||
'invalid_mapping' => 'Отображение, предусмотренное для :port, было недействительным и не могло быть обработано.',
|
||||
'cidr_out_of_range' => 'Нотация CIDR допускает маски только от / 25 до / 32.',
|
||||
'port_out_of_range' => 'Порты в распределении должны быть больше 1024 и меньше или равны 65535.',
|
||||
],
|
||||
'nest' => [
|
||||
'delete_has_servers' => 'Гнездо с подключенными к нему активными серверами нельзя удалить с панели.',
|
||||
'egg' => [
|
||||
'delete_has_servers' => 'Яйцо с подключенными к нему активными серверами нельзя удалить с панели.',
|
||||
'invalid_copy_id' => 'Яйцо, выбранное для копирования скрипта, либо не существует, либо копирует сам скрипт.',
|
||||
'must_be_child' => 'Директива "Copy Settings From" для этого яйца должна быть дочерней для выбранного гнезда.',
|
||||
'has_children' => 'Это яйцо является родителем одного или нескольких других яиц. Пожалуйста, удалите эти яйца перед удалением этого яйца.',
|
||||
],
|
||||
'variables' => [
|
||||
'env_not_unique' => 'Переменная окружения :name должно быть уникальным для этого яйца.',
|
||||
'reserved_name' => 'Переменная среды :name защищено и не может быть присвоено переменной.',
|
||||
'bad_validation_rule' => 'Правило проверки ":rule" не является допустимым правилом для этого приложения.',
|
||||
],
|
||||
'importer' => [
|
||||
'json_error' => 'Произошла ошибка при попытке проанализировать файл JSON: :error.',
|
||||
'file_error' => 'Предоставленный файл JSON недействителен.',
|
||||
'invalid_json_provided' => 'Предоставленный файл JSON не в формате, который может быть распознан.',
|
||||
],
|
||||
],
|
||||
'packs' => [
|
||||
'delete_has_servers' => 'Невозможно удалить пакет, прикрепленный к активным серверам.',
|
||||
'update_has_servers' => 'Невозможно изменить идентификатор связанной опции, когда серверы в данный момент подключены к пакету.',
|
||||
'invalid_upload' => 'Предоставленный файл не является действительным.',
|
||||
'invalid_mime' => 'Предоставленный файл не соответствует требуемому типу :type',
|
||||
'unreadable' => 'Предоставленный архив не может быть открыт сервером.',
|
||||
'zip_extraction' => 'Возникла исключительная ситуация при попытке извлечь архив, предоставленный на сервер.',
|
||||
'invalid_archive_exception' => 'В архиве пакета отсутствует требуемый файл archive.tar.gz или import.json в базовом каталоге.',
|
||||
],
|
||||
'subusers' => [
|
||||
'editing_self' => 'Редактирование вашей учетной записи подпользователя запрещено.',
|
||||
'user_is_owner' => 'Вы не можете добавить владельца сервера в качестве подпользователя для этого сервера.',
|
||||
'subuser_exists' => 'Пользователь с таким адресом электронной почты уже назначен в качестве подпользователя для этого сервера.',
|
||||
],
|
||||
'databases' => [
|
||||
'delete_has_databases' => 'Невозможно удалить хост-сервер базы данных, с которым связаны активные базы данных.',
|
||||
],
|
||||
'tasks' => [
|
||||
'chain_interval_too_long' => 'Максимальный интервал времени для связанной задачи составляет 15 минут.',
|
||||
],
|
||||
'locations' => [
|
||||
'has_nodes' => 'Невозможно удалить местоположение, к которому прикреплены активные узлы.',
|
||||
],
|
||||
'users' => [
|
||||
'node_revocation_failed' => 'Не удалось отозвать ключи на <a href=":link">Node #:node</a>. :error',
|
||||
],
|
||||
'deployment' => [
|
||||
'no_viable_nodes' => 'Узлы, удовлетворяющие требованиям, указанным для автоматического развертывания, не найдены.',
|
||||
'no_viable_allocations' => 'Местоположений, удовлетворяющих требованиям для автоматического развертывания, обнаружено не было.',
|
||||
],
|
||||
'api' => [
|
||||
'resource_not_found' => 'Запрашиваемый ресурс не существует на этом сервере.',
|
||||
],
|
||||
];
|
32
resources/lang/ru/navigation.php
Normal file
32
resources/lang/ru/navigation.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'home' => 'Главная',
|
||||
'account' => [
|
||||
'header' => 'УПРАВЛЕНИЕ АККАУНТОМ',
|
||||
'my_account' => 'Мой аккаунт',
|
||||
'security_controls' => 'Контроль безопасности',
|
||||
'api_access' => 'API аккаунта',
|
||||
'my_servers' => 'Мои серверы',
|
||||
],
|
||||
'server' => [
|
||||
'header' => 'УПРАВЛЕНИЕ СЕРВЕРАМИ',
|
||||
'console' => 'Консоль',
|
||||
'console-pop' => 'Полноэкранная консоль',
|
||||
'file_management' => 'Управление файлами',
|
||||
'file_browser' => 'Браузер файлов',
|
||||
'create_file' => 'Создать файл',
|
||||
'upload_files' => 'Загрузить файлы',
|
||||
'subusers' => 'Подпользователь',
|
||||
'schedules' => 'Расписание',
|
||||
'configuration' => 'Конфигурация',
|
||||
'port_allocations' => 'Настройки портов',
|
||||
'sftp_settings' => 'Настройки SFTP',
|
||||
'startup_parameters' => 'Параметры запуска',
|
||||
'databases' => 'Базы данных',
|
||||
'edit_file' => 'Редактировать файл',
|
||||
'admin_header' => 'АДМИНИСТРАТИВНЫЕ',
|
||||
'admin' => 'Конфигурация сервера',
|
||||
'server_name' => 'Название сервера',
|
||||
],
|
||||
];
|
17
resources/lang/ru/pagination.php
Normal file
17
resources/lang/ru/pagination.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Предыдущий',
|
||||
'next' => 'Следующий »',
|
||||
];
|
19
resources/lang/ru/passwords.php
Normal file
19
resources/lang/ru/passwords.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| has failed, such as for an invalid token or invalid new password.
|
||||
|
|
||||
*/
|
||||
'password' => 'Пароли должны содержать не менее шести символов и соответствовать подтверждению.',
|
||||
'reset' => 'Ваш пароль сброшен!',
|
||||
'sent' => 'Мы отправили вам ссылку для сброса пароля по электронной почте!',
|
||||
'token' => 'Этот токен сброса пароля недействителен.',
|
||||
'user' => 'Мы не можем найти пользователя с таким адресом электронной почты.',
|
||||
];
|
334
resources/lang/ru/server.php
Normal file
334
resources/lang/ru/server.php
Normal file
|
@ -0,0 +1,334 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'index' => [
|
||||
'title' => 'Просмотр сервера :name',
|
||||
'header' => 'Консоль сервера',
|
||||
'header_sub' => 'Контролируйте свой сервер в режиме реального времени.',
|
||||
],
|
||||
'schedule' => [
|
||||
'header' => 'Диспетчер расписаний',
|
||||
'header_sub' => 'Управляйте всеми расписаниями этого сервера в одном месте.',
|
||||
'current' => 'Текущие расписания',
|
||||
'new' => [
|
||||
'header' => 'Создать новое расписание',
|
||||
'header_sub' => 'Создайте новый набор запланированных задач для этого сервера.',
|
||||
'submit' => 'Создать расписание',
|
||||
],
|
||||
'manage' => [
|
||||
'header' => 'Управление расписанием',
|
||||
'submit' => 'Обновить расписание',
|
||||
'delete' => 'Удалить расписание',
|
||||
],
|
||||
'task' => [
|
||||
'time' => 'После',
|
||||
'action' => 'Выполнить действие',
|
||||
'payload' => 'С полезной нагрузкой',
|
||||
'add_more' => 'Добавить еще одну задачу',
|
||||
],
|
||||
'actions' => [
|
||||
'command' => 'Отправить команду',
|
||||
'power' => 'Управление питанием',
|
||||
],
|
||||
'toggle' => 'Переключить статус',
|
||||
'run_now' => 'Расписание запуска',
|
||||
'schedule_created' => 'Успешно создано новое расписание для этого сервера.',
|
||||
'schedule_updated' => 'Расписание обновлено.',
|
||||
'unnamed' => 'Расписание без имени',
|
||||
'setup' => 'Настройка расписания',
|
||||
'day_of_week' => 'День недели',
|
||||
'day_of_month' => 'День месяца',
|
||||
'hour' => 'Час дня',
|
||||
'minute' => 'Минута часа',
|
||||
'time_help' => 'Система расписаний поддерживает использование синтаксиса Cronjob при определении момента начала выполнения задач. Используйте поля выше, чтобы указать, когда эти задачи должны начать выполняться, или выберите параметры в меню множественного выбора.',
|
||||
'task_help' => 'Время выполнения заданий относительно ранее определенного задания. Каждому расписанию может быть назначено не более 5 задач, а задачи не могут быть запланированы с интервалом более 15 минут.',
|
||||
],
|
||||
'tasks' => [
|
||||
'task_created' => 'Успешно создано новое задание на панели.',
|
||||
'task_updated' => 'Задача успешно обновлена. Любые действия задачи, поставленные в очередь, будут отменены и запущены снова в следующее определенное время.',
|
||||
'header' => 'Запланированные задачи',
|
||||
'header_sub' => 'Автоматизируйте свой сервер.',
|
||||
'current' => 'Текущие запланированные задачи',
|
||||
'actions' => [
|
||||
'command' => 'Отправить команду',
|
||||
'power' => 'Опция управленя питанием ',
|
||||
],
|
||||
'new_task' => 'Добавить новую задачу',
|
||||
'toggle' => 'Переключить статус',
|
||||
'new' => [
|
||||
'header' => 'Новое задание',
|
||||
'header_sub' => 'Создайте новое запланированное задание для этого сервера.',
|
||||
'task_name' => 'Название задачи',
|
||||
'day_of_week' => 'День недели',
|
||||
'custom' => 'Пользовательское значение',
|
||||
'day_of_month' => 'День месяца',
|
||||
'hour' => 'Час',
|
||||
'minute' => 'Минута',
|
||||
'sun' => 'Воскресенье',
|
||||
'mon' => 'Понедельник',
|
||||
'tues' => 'Вторник',
|
||||
'wed' => 'Среда',
|
||||
'thurs' => 'Четверг',
|
||||
'fri' => 'Пятница',
|
||||
'sat' => 'Суббота',
|
||||
'submit' => 'Создать задачу',
|
||||
'type' => 'Тип задачи',
|
||||
'chain_then' => 'Затем после',
|
||||
'chain_do' => 'Выполнять',
|
||||
'chain_arguments' => 'С аргументами',
|
||||
'payload' => 'Задача Полезная нагрузка',
|
||||
'payload_help' => 'Например, если вы выбрали <code>Send Command</code>, введите команду здесь. Если вы выбрали <code>Send Power Option</code>, укажите здесь действие power (например, <code>restart</code>).',
|
||||
],
|
||||
'edit' => [
|
||||
'header' => 'Управление задачей',
|
||||
'submit' => 'Обновить задачу',
|
||||
],
|
||||
],
|
||||
'users' => [
|
||||
'header' => 'Управление пользователями',
|
||||
'header_sub' => 'Контроль, кто может получить доступ к вашему серверу.',
|
||||
'configure' => 'Настроить разрешения',
|
||||
'list' => 'Аккаунты с доступом',
|
||||
'add' => 'Добавить нового пользователя',
|
||||
'update' => 'Обновить пользователя',
|
||||
'user_assigned' => 'Успешно назначен новый пользователь на этот сервер.',
|
||||
'user_updated' => 'Успешно обновлены разрешения.',
|
||||
'edit' => [
|
||||
'header' => 'Редактировать пользователя',
|
||||
'header_sub' => 'Изменить доступ пользователя к серверу.',
|
||||
],
|
||||
'new' => [
|
||||
'header' => 'Добавить нового пользователя',
|
||||
'header_sub' => 'Добавьте нового пользователя с разрешениями на этот сервер.',
|
||||
'email' => 'Адрес электронной почты',
|
||||
'email_help' => 'Введите адрес электронной почты для пользователя, которого вы хотите пригласить для управления этим сервером.',
|
||||
'power_header' => 'Управление питанием',
|
||||
'file_header' => 'Управление файлами',
|
||||
'subuser_header' => 'Управление пользователями',
|
||||
'server_header' => 'Управление сервером',
|
||||
'task_header' => 'Управление расписанием',
|
||||
'database_header' => 'Управление базой данных',
|
||||
'power_start' => [
|
||||
'title' => 'Запустить сервер',
|
||||
'description' => 'Позволяет пользователю запустить сервер.',
|
||||
],
|
||||
'power_stop' => [
|
||||
'title' => 'Остановить сервер.',
|
||||
'description' => 'Позволяет пользователю остановить сервер.',
|
||||
],
|
||||
'power_restart' => [
|
||||
'title' => 'Перезагрузить сервер',
|
||||
'description' => 'Позволяет пользователю перезапустить сервер.',
|
||||
],
|
||||
'power_kill' => [
|
||||
'title' => 'Убить Сервер',
|
||||
'description' => 'Позволяет пользователю убить процесс сервера.',
|
||||
],
|
||||
'send_command' => [
|
||||
'title' => 'Отправить консольную команду',
|
||||
'description' => 'Позволяет отправить команду из консоли. Если у пользователя нет разрешений на остановку или перезапуск, он не может отправить команду stop',
|
||||
],
|
||||
'access_sftp' => [
|
||||
'title' => 'SFTP разрешения',
|
||||
'description' => 'Позволяет пользователю подключаться к SFTP-серверу, предоставленному демоном.',
|
||||
],
|
||||
'list_files' => [
|
||||
'title' => 'Список файлов',
|
||||
'description' => 'Позволяет пользователю перечислять все файлы и папки на сервере, но не просматривать содержимое файла.',
|
||||
],
|
||||
'edit_files' => [
|
||||
'title' => 'Редактировать файлы',
|
||||
'description' => 'Позволяет пользователю открыть файл только для просмотра. SFTP не влияет на это разрешение.',
|
||||
],
|
||||
'save_files' => [
|
||||
'title' => 'Сохранить файлы',
|
||||
'description' => 'По',
|
||||
],
|
||||
'move_files' => [
|
||||
'title' => 'Переименовать и переместить файлы',
|
||||
'description' => 'Позволяет пользователю перемещать и переименовывать файлы и папки в файловой системе.',
|
||||
],
|
||||
'copy_files' => [
|
||||
'title' => 'Копировать файлы',
|
||||
'description' => 'Позволяет пользователю копировать файлы и папки в файловой системе.',
|
||||
],
|
||||
'compress_files' => [
|
||||
'title' => 'Сжатие файлов',
|
||||
'description' => 'Позволяет пользователю создавать архивы файлов и папок в системе.',
|
||||
],
|
||||
'decompress_files' => [
|
||||
'title' => 'Распаковать файлы',
|
||||
'description' => 'Позволяет пользователю распаковать архивы .zip и .tar (.gz).',
|
||||
],
|
||||
'create_files' => [
|
||||
'title' => 'Создать файлы',
|
||||
'description' => 'Позволяет пользователю создавать новый файл в панели.',
|
||||
],
|
||||
'upload_files' => [
|
||||
'title' => 'Загрузить файлы',
|
||||
'description' => 'Позволяет пользователю загружать файлы через файловый менеджер.',
|
||||
],
|
||||
'delete_files' => [
|
||||
'title' => 'Удалить файлы',
|
||||
'description' => 'Позволяет пользователю удалять файлы из системы.',
|
||||
],
|
||||
'download_files' => [
|
||||
'title' => 'Скачать файлы',
|
||||
'description' => 'Позволяет пользователю загружать файлы. Если пользователю дано это разрешение, он может загружать и просматривать содержимое файла, даже если это разрешение не назначено на панели.',
|
||||
],
|
||||
'list_subusers' => [
|
||||
'title' => 'Список пользователей',
|
||||
'description' => 'Позволяет пользователю просматривать список всех суб-пользователей, назначенных серверу.',
|
||||
],
|
||||
'view_subuser' => [
|
||||
'title' => 'Просмотр пользователей',
|
||||
'description' => 'Позволяет пользователю просматривать разрешения, назначенные для пользователей.',
|
||||
],
|
||||
'edit_subuser' => [
|
||||
'title' => 'Редактировать пользователей',
|
||||
'description' => 'Позволяет пользователю редактировать разрешения, назначенные другим пользователям.',
|
||||
],
|
||||
'create_subuser' => [
|
||||
'title' => 'Создать пользователя',
|
||||
'description' => 'Позволяет пользователю создавать дополнительных пользователей на сервере.',
|
||||
],
|
||||
'delete_subuser' => [
|
||||
'title' => 'Удалить пользователя',
|
||||
'description' => 'Позволяет пользователю удалять других пользователей на сервере.',
|
||||
],
|
||||
'view_allocations' => [
|
||||
'title' => 'Посмотреть распределение',
|
||||
'description' => 'Позволяет пользователю просматривать все IP-адреса и порты, назначенные серверу.',
|
||||
],
|
||||
'edit_allocation' => [
|
||||
'title' => 'Изменить подключение по умолчанию',
|
||||
'description' => 'Позволяет пользователю изменять распределение соединения по умолчанию для использования на сервере.',
|
||||
],
|
||||
'view_startup' => [
|
||||
'title' => 'Просмотреть команду запуска',
|
||||
'description' => 'Позволяет пользователю просматривать команду запуска и связанные переменные для сервера.',
|
||||
],
|
||||
'edit_startup' => [
|
||||
'title' => 'Редактировать команду запуска',
|
||||
'description' => 'Позволяет пользователю изменять переменные запуска для сервера.',
|
||||
],
|
||||
'list_schedules' => [
|
||||
'title' => 'Список расписаний',
|
||||
'description' => 'Позволяет пользователю посмотреть все расписания (включенные и отключенные) для этого сервера.',
|
||||
],
|
||||
'view_schedule' => [
|
||||
'title' => 'Посмотреть расписание',
|
||||
'description' => 'Позволяет пользователю просматривать детали конкретного расписания, включая все назначенные задачи.',
|
||||
],
|
||||
'toggle_schedule' => [
|
||||
'title' => 'Переключить Расписание',
|
||||
'description' => 'Позволяет пользователю переключать расписание на активные или неактивные.',
|
||||
],
|
||||
'queue_schedule' => [
|
||||
'title' => 'Очередь Расписаний',
|
||||
'description' => 'Позволяет пользователю ставить в очередь расписание для выполнения его задач в следующем цикле процесса.',
|
||||
],
|
||||
'edit_schedule' => [
|
||||
'title' => 'Изменить расписание',
|
||||
'description' => 'Позволяет пользователю редактировать расписание, включая все задачи расписания. Это позволит пользователю удалять отдельные задачи, но не удалять само расписание.',
|
||||
],
|
||||
'create_schedule' => [
|
||||
'title' => 'Создать расписание',
|
||||
'description' => 'Позволяет пользователю создавать новое расписание.',
|
||||
],
|
||||
'delete_schedule' => [
|
||||
'title' => 'Удалить расписание',
|
||||
'description' => 'Позволяет пользователю удалить расписание с сервера.',
|
||||
],
|
||||
'view_databases' => [
|
||||
'title' => 'Просмотр базы данных',
|
||||
'description' => 'Позволяет пользователю просматривать все базы данных, связанные с этим сервером, включая имена пользователей и пароли для баз данных.',
|
||||
],
|
||||
'reset_db_password' => [
|
||||
'title' => 'Сбросить пароль базы данных',
|
||||
'description' => 'Позволяет пользователю сбросить пароли для баз данных.',
|
||||
],
|
||||
'delete_database' => [
|
||||
'title' => 'Удалить базы данных',
|
||||
'description' => 'Позволяет пользователю удалять базы данных для этого сервера.',
|
||||
],
|
||||
'create_database' => [
|
||||
'title' => 'Создать базу данных',
|
||||
'description' => 'Позволяет пользователю создавать дополнительные базы данных для этого сервера.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'allocations' => [
|
||||
'mass_actions' => 'Массовые акции',
|
||||
'delete' => 'Удалить распределение',
|
||||
],
|
||||
'files' => [
|
||||
'exceptions' => [
|
||||
'invalid_mime' => 'Этот тип файла не может быть отредактирован через встроенный редактор.',
|
||||
'max_size' => 'Этот файл слишком велик для редактирования через встроенный редактор.',
|
||||
],
|
||||
'header' => 'Файловый менеджер',
|
||||
'header_sub' => 'Управляйте всеми своими файлами прямо из Интернета.',
|
||||
'loading' => 'Загрузка исходной файловой структуры, это может занять несколько секунд.',
|
||||
'path' => 'При настройке любых путей к файлам в плагинах или настройках вашего сервера вы должны использовать :path в качестве базового пути. Максимальный размер загрузки файлов через Интернет на этот узел: :size.',
|
||||
'seconds_ago' => 'секунд назад',
|
||||
'file_name' => 'Имя файла',
|
||||
'size' => 'Размер',
|
||||
'last_modified' => 'Последнее изменение',
|
||||
'add_new' => 'Добавить новый файл',
|
||||
'add_folder' => 'Добавить новую папку',
|
||||
'mass_actions' => 'Массовые акции',
|
||||
'delete' => 'Удалить файлы',
|
||||
'edit' => [
|
||||
'header' => 'Редактировать файл',
|
||||
'header_sub' => 'Внесите изменения в файл из Интернета.',
|
||||
'save' => 'Сохранить файл',
|
||||
'return' => 'Вернуться в файловый менеджер',
|
||||
],
|
||||
'add' => [
|
||||
'header' => 'Новый файл',
|
||||
'header_sub' => 'Создайте новый файл на вашем сервере.',
|
||||
'name' => 'Имя файла',
|
||||
'create' => 'Создать файл',
|
||||
],
|
||||
],
|
||||
'config' => [
|
||||
'name' => [
|
||||
'header' => 'Название сервера',
|
||||
'header_sub' => 'Измените имя этого сервера.',
|
||||
'details' => 'Имя сервера является только ссылкой на этот сервер на панели и не влияет на какие-либо конкретные конфигурации серверов, которые могут отображаться для пользователей в играх.',
|
||||
],
|
||||
'startup' => [
|
||||
'header' => 'Начать настройку',
|
||||
'header_sub' => 'Управляйте аргументами запуска сервера.',
|
||||
'command' => 'Команда запуска',
|
||||
'edit_params' => 'Изменить параметры',
|
||||
'update' => 'Обновить параметры запуска',
|
||||
'startup_regex' => 'Правила ввода',
|
||||
'edited' => 'Переменные запуска были успешно отредактированы. Они вступят в силу при следующем запуске этого сервера.',
|
||||
],
|
||||
'sftp' => [
|
||||
'header' => 'Конфигурация SFTP',
|
||||
'header_sub' => 'Данные учетной записи для SFTP-соединений.',
|
||||
'details' => 'Подробности SFTP',
|
||||
'conn_addr' => 'Адрес подключения',
|
||||
'warning' => 'Пароль SFTP - это пароль вашей учетной записи. Убедитесь, что ваш клиент настроен на использование SFTP, а не FTP или FTPS для соединений, между протоколами есть разница.',
|
||||
],
|
||||
'database' => [
|
||||
'header' => 'Базы данных',
|
||||
'header_sub' => 'Все базы данных доступные для этого сервера.',
|
||||
'your_dbs' => 'Настройки базы данных',
|
||||
'host' => 'MySQL Хост',
|
||||
'reset_password' => 'Сброс пароля',
|
||||
'no_dbs' => 'Для этого сервера нет баз данных.',
|
||||
'add_db' => 'Добавить новую базу данных.',
|
||||
],
|
||||
'allocation' => [
|
||||
'header' => 'Порты сервера',
|
||||
'header_sub' => 'Управляйте IP-адресами и портами, доступными на этом сервере.',
|
||||
'available' => 'Доступные Распределения',
|
||||
'help' => 'Справка по Распределению портов.',
|
||||
'help_text' => 'Список включает в себя все доступные IP-адреса и порты, которые открыты для вашего сервера, чтобы использовать для входящих подключений.',
|
||||
],
|
||||
],
|
||||
];
|
88
resources/lang/ru/strings.php
Normal file
88
resources/lang/ru/strings.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'email' => 'Эл. адрес',
|
||||
'user_identifier' => 'Имя пользователя или адрес электронной почты',
|
||||
'password' => 'Пароль',
|
||||
'confirm_password' => 'Подтвердите Пароль',
|
||||
'login' => 'Войти',
|
||||
'home' => 'Домой',
|
||||
'servers' => 'Серверы',
|
||||
'id' => 'ID',
|
||||
'name' => 'Название',
|
||||
'node' => 'Узел',
|
||||
'connection' => 'Подключение',
|
||||
'memory' => 'Память',
|
||||
'cpu' => 'Процессор',
|
||||
'status' => 'Статус',
|
||||
'search' => 'Поиск',
|
||||
'suspended' => 'Приостановленный',
|
||||
'account' => 'Аккаунт',
|
||||
'security' => 'Безопасность',
|
||||
'ip' => 'IP адрес',
|
||||
'last_activity' => 'Последняя активность',
|
||||
'revoke' => 'Отменить',
|
||||
'2fa_token' => 'Токен аутентификации',
|
||||
'submit' => 'Отправить',
|
||||
'close' => 'Закрыть',
|
||||
'settings' => 'Настройки',
|
||||
'configuration' => 'Конфигурация',
|
||||
'sftp' => 'SFTP',
|
||||
'databases' => 'База данных',
|
||||
'memo' => 'Напоминание',
|
||||
'created' => 'Созданный',
|
||||
'expires' => 'Истекает',
|
||||
'public_key' => 'Токен',
|
||||
'api_access' => 'Доступ к API',
|
||||
'never' => 'никогда',
|
||||
'sign_out' => 'Выход',
|
||||
'admin_control' => 'Админ панель',
|
||||
'required' => 'Необходимо',
|
||||
'port' => 'Порт',
|
||||
'username' => 'Имя пользователя',
|
||||
'database' => 'База данных',
|
||||
'new' => 'Новый',
|
||||
'danger' => 'Опасность',
|
||||
'create' => 'Создать',
|
||||
'select_all' => 'Выбрать все',
|
||||
'select_none' => 'Выберат ни одного',
|
||||
'alias' => 'Псевдоним',
|
||||
'primary' => 'Основной',
|
||||
'make_primary' => 'Сделать основным',
|
||||
'none' => 'Никто',
|
||||
'cancel' => 'Отмена',
|
||||
'created_at' => 'Создан в',
|
||||
'action' => 'Действие',
|
||||
'data' => 'Дата',
|
||||
'queued' => 'Очередь',
|
||||
'last_run' => 'Последний запуск',
|
||||
'next_run' => 'Следующий запуск',
|
||||
'not_run_yet' => 'Еще не работает',
|
||||
'yes' => 'Да',
|
||||
'no' => 'Нет',
|
||||
'delete' => 'Удалить',
|
||||
'2fa' => '2FA',
|
||||
'logout' => 'Выйти',
|
||||
'admin_cp' => 'Панель управления администратора',
|
||||
'optional' => 'Опциональный',
|
||||
'read_only' => 'Только чтение',
|
||||
'relation' => 'Зависимость',
|
||||
'owner' => 'Владелец',
|
||||
'admin' => 'Администратор',
|
||||
'subuser' => 'Доп-пользователь',
|
||||
'captcha_invalid' => 'Капча недействительна.',
|
||||
'tasks' => 'Задачи',
|
||||
'seconds' => 'Секунд',
|
||||
'minutes' => 'Минут',
|
||||
'under_maintenance' => 'На техобслуживании',
|
||||
'days' => [
|
||||
'sun' => 'Воскресенье',
|
||||
'mon' => 'Понедельник',
|
||||
'tues' => 'Вторник',
|
||||
'wed' => 'Среда',
|
||||
'thurs' => 'Черверг',
|
||||
'fri' => 'Пятница',
|
||||
'sat' => 'Суббота',
|
||||
],
|
||||
'last_used' => 'Последний раз был использован',
|
||||
];
|
105
resources/lang/ru/validation.php
Normal file
105
resources/lang/ru/validation.php
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| following language lines contain default error messages used by
|
||||
| validator class. Some of these rules have multiple versions such
|
||||
| as size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => ':attribute должен быть принят.',
|
||||
'active_url' => ':attribute не является допустимым URL.',
|
||||
'after' => ':attribute должен быть датой после :date.',
|
||||
'after_or_equal' => ':attribute должен быть датой после или равен :date.',
|
||||
'alpha' => ':attribute может содержать только буквы.',
|
||||
'alpha_dash' => ':attribute может содержать только буквы, цифры и тире.',
|
||||
'alpha_num' => ':attribute может содержать только буквы и цифры.',
|
||||
'array' => ':attribute должен быть массивом.',
|
||||
'before' => ':attribute должен быть датой до :date.',
|
||||
'before_or_equal' => ':attribute должен быть датой до или равен :date.',
|
||||
'between' => [
|
||||
'numeric' => ':attribute должен быть между :min и :max.',
|
||||
'file' => ':attribute должен быть между :min и :max килобайт.',
|
||||
'string' => ':attribute должен быть между :min и :max символами.',
|
||||
'array' => ':attribute должен содержать от :min до :max элементов.',
|
||||
],
|
||||
'boolean' => ':attribute должно быть true или false.',
|
||||
'confirmed' => ':attribute Подтверждение не совпадает.',
|
||||
'date' => ':attribute не является допустимой датой.',
|
||||
'date_format' => ':attribute не соответствует формату :format.',
|
||||
'different' => ':attribute и :other должен быть другим.',
|
||||
'digits' => ':attribute должен быть :digits цифровым.',
|
||||
'digits_between' => ':attribute должен быть между :min и :max цифрами.',
|
||||
'dimensions' => ':attribute имеет недопустимые размеры изображения.',
|
||||
'distinct' => ':attribute имеет повторяющееся значение.',
|
||||
'email' => ':attribute должен быть действительным адресом электронной почты.',
|
||||
'exists' => 'выбранный :attribute недействителен.',
|
||||
'file' => ':attribute должен быть файлом.',
|
||||
'filled' => 'поле :attribute обязательно для заполнения.',
|
||||
'image' => ':attribute должен быть изображением.',
|
||||
'in' => 'выбранный :attribute недействителен.',
|
||||
'in_array' => 'поле :attribute не существует в :other.',
|
||||
'integer' => ':attribute должен быть целым числом.',
|
||||
'ip' => ':attribute должен быть действительным IP-адресом.',
|
||||
'json' => ':attribute должен быть допустимой строкой JSON.',
|
||||
'max' => [
|
||||
'numeric' => ':attribute не может быть больше чем :max.',
|
||||
'file' => ':attribute не может быть больше, чем :max килобайт.',
|
||||
'string' => ':attribute не может быть больше, чем :max символов.',
|
||||
'array' => ':attribute может содержать не более :max предметов.',
|
||||
],
|
||||
'mimes' => ':attribute должен быть файл типа: :values.',
|
||||
'mimetypes' => ':attribute должен быть файл типа: :values.',
|
||||
'min' => [
|
||||
'numeric' => ':attribute должен быть не менее :min.',
|
||||
'file' => ':attribute должно быть не менее :min килобайт.',
|
||||
'string' => ':attribute должно быть не менее :min символов.',
|
||||
'array' => ':attribute должно иметь как минимум :min предметов.',
|
||||
],
|
||||
'not_in' => 'выбранный :attribute недействителен.',
|
||||
'numeric' => ':attribute должен быть числом.',
|
||||
'present' => ':attribute поле должно присутствовать.',
|
||||
'regex' => ':attribute Формат неверен.',
|
||||
'required' => ':attribute Поле, обязательное для заполнения.',
|
||||
'required_if' => ':attribute Поле обязательно для заполнения, когда :other является :value.',
|
||||
'required_unless' => ':attribute Поле обязательно для заполнения, если :other не находится в :values.',
|
||||
'required_with' => ':attribute Поле обязательно для заполнения, когда :values присутствуют.',
|
||||
'required_with_all' => ':attribute Поле обязательно для заполнения, когда :values присутствуют.',
|
||||
'required_without' => ':attribute Поле обязательно для заполнения, когда :values отсутствуют.',
|
||||
'required_without_all' => ':attribute поле обязательно для заполнения, когда нет ни одного из :values.',
|
||||
'same' => ':attribute и :other должены совпадать.',
|
||||
'size' => [
|
||||
'numeric' => ':attribute должно быть :size.',
|
||||
'file' => ':attribute должно быть :size килобайт.',
|
||||
'string' => ':attribute должно быть :size символов.',
|
||||
'array' => ':attribute должен содержать :size предметов.',
|
||||
],
|
||||
'string' => ':attribute должен быть строкой.',
|
||||
'timezone' => ':attribute должна быть действительной зоной.',
|
||||
'unique' => ':attribute уже занят.',
|
||||
'uploaded' => ':attribute не удалось загрузить.',
|
||||
'url' => ':attribute Формат неверен.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| following language lines are used to swap attribute place-holders
|
||||
| with something more reader friendly such as E-Mail Address instead
|
||||
| of "email". This simply helps us make messages a little cleaner.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
// Internal validation logic for Pterodactyl
|
||||
'internal' => [
|
||||
'variable_value' => ':env переменная',
|
||||
],
|
||||
];
|
|
@ -9,25 +9,25 @@
|
|||
|
||||
return [
|
||||
'notices' => [
|
||||
'created' => '一个新的管理模块, :name, 已成功创建。',
|
||||
'deleted' => '成功从面板删除指定的管理模块。',
|
||||
'updated' => '成功更新管理模块的选项。',
|
||||
'created' => '已成功创建 :name 。',
|
||||
'deleted' => '已成功从面板删除指定的管理模块。',
|
||||
'updated' => '已成功更新管理模块选项。',
|
||||
],
|
||||
'eggs' => [
|
||||
'notices' => [
|
||||
'imported' => '成功导入一个管理模板。',
|
||||
'updated_via_import' => '该管理模板已按照上传的文件完成更新。',
|
||||
'deleted' => '成功删除指定的管路模板。',
|
||||
'updated' => '成功更新管理模板的配置。',
|
||||
'script_updated' => '管理模板的安装脚本已经成功更新并且会在安装新服务器时被执行。',
|
||||
'imported' => '已成功导入管理模板。',
|
||||
'updated_via_import' => '此管理模板已按照上传的文件完成更新。',
|
||||
'deleted' => '已成功删除指定的管路模板。',
|
||||
'updated' => '已成功更新管理模板的配置。',
|
||||
'script_updated' => '已成功更新孵化蛋安装脚本且将于服务器安装时自动执行。',
|
||||
'egg_created' => '一个管理模板已经成功创建. 你需要重启所有正在运行的节点受控端来使该模板生效。',
|
||||
],
|
||||
],
|
||||
'variables' => [
|
||||
'notices' => [
|
||||
'variable_deleted' => '参数 ":variable" 已被移除,在服务器重装之后将不在有效。',
|
||||
'variable_updated' => '参数 ":variable" 已更新。 你需要重装所有服务器来使该参数生效.',
|
||||
'variable_created' => '新的参数已经创建并被赋值,该操作会影响此管理模板下的所有服务器',
|
||||
'variable_deleted' => '已移除变量 ":variable" 且其在重构服务器镜像后将会失效。 ',
|
||||
'variable_updated' => '已更新变量 ":variable" 。您需要重构使用此变量的服务器以应用更改。',
|
||||
'variable_created' => '已成功创建新变量并分配给此孵化蛋。',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -9,15 +9,15 @@
|
|||
|
||||
return [
|
||||
'validation' => [
|
||||
'fqdn_not_resolvable' => '提供的域名或地址没有解析到一个合法的IP地址.',
|
||||
'fqdn_required_for_ssl' => '这个节点要求解析到一个公共IP的域名必须使用SSL',
|
||||
'fqdn_not_resolvable' => '提供的正式域名(FQDN)或 IP 地址未解析到有效的 IP 地址。',
|
||||
'fqdn_required_for_ssl' => '此节点需要解析到公网 IP 地址的正式域名才能使用 SSL。',
|
||||
],
|
||||
'notices' => [
|
||||
'allocations_added' => '配额已经成功的被添加到这个节点.',
|
||||
'node_deleted' => '节点成功从面板中移除.',
|
||||
'location_required' => '在你可以添加一个节点之前必须至少有一个可用区配置。',
|
||||
'node_created' => '节点新建成功! 使用 \'Configuration\' 标签,你可以在此节点上自动配置受控端. <strong>在你可以创建服务器之前,你必须至少分配一个IP和端口</strong>',
|
||||
'node_updated' => '节点信息更新成功!如果任何节点受控端的设置更改了,您需要重启受控端来使设置生效.',
|
||||
'unallocated_deleted' => '已删除 <code>:ip</code> 上的所有未分配的端口',
|
||||
'allocations_added' => '已成功为此节点分配地址。',
|
||||
'node_deleted' => '已成功从面板中移除节点。',
|
||||
'location_required' => '您必须至少配置一个区域才能添加节点至面板。',
|
||||
'node_created' => '已成功新建节点!您可通过\'配置\'选项卡已自动配置此机器上的守护程序。<strong>在您添加服务器前,您必须先分配一个 IP 地址及端口。</strong>',
|
||||
'node_updated' => '已更新节点信息。若守护程序设置更改,您需要重启守护程序才能生效。',
|
||||
'unallocated_deleted' => '已为 <code>:ip</code> 删除所有未分配的端口。',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
return [
|
||||
'notices' => [
|
||||
'pack_updated' => '整合包已经被更新。',
|
||||
'pack_deleted' => '成功删除整合包: ":name" 。',
|
||||
'pack_created' => '一个整合包已被成功创建,现在可以用它来部署服务器了。',
|
||||
'pack_updated' => '已成功更新整合包。',
|
||||
'pack_deleted' => '已成功从系统中删除整合包 “:name”。',
|
||||
'pack_created' => '已成功在系统上创建整合包,您现在可以使用它来部署服务器了。',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -9,23 +9,23 @@
|
|||
|
||||
return [
|
||||
'exceptions' => [
|
||||
'no_new_default_allocation' => '你正在尝试删除此服务器的默认配额,但是该服务器没有足够的后备配额。',
|
||||
'marked_as_failed' => '这个服务器目前被标记为安装失败。 当前状态不能改变为此状态。',
|
||||
'bad_variable' => '变量 :name 有一个已确认的错误 。',
|
||||
'daemon_exception' => '连接受控端时发生意外 返回错误码 HTTP/:code response code. 此错误已被记录。',
|
||||
'default_allocation_not_found' => '请求的默认配额没有在这台服务器上找到。',
|
||||
'no_new_default_allocation' => '您正在尝试删除此服务器的默认分配地址,但此服务器可用的备选分配地址。',
|
||||
'marked_as_failed' => '此服务器被标记为安装失败。当前状态无法在面板中被改变。',
|
||||
'bad_variable' => '变量 :name 有验证错误。',
|
||||
'daemon_exception' => '连接守护程序时返回 HTTP/:code 反馈码。此错误已被记录。',
|
||||
'default_allocation_not_found' => '未在此服务器上找到请求的默认分配地址。',
|
||||
],
|
||||
'alerts' => [
|
||||
'startup_changed' => '该服务器的启动配置已被更新. 如果此服务器所属的管理模块或管理模板更改,此时将发生一次配置重设',
|
||||
'server_deleted' => '成功从系统中删除服务器',
|
||||
'server_created' => '创建服务器成功。 请稍后几分钟,受控端将尽快完成服务器安装',
|
||||
'build_updated' => '启动参数已更改。 一些修改需要重启该服务器后生效。',
|
||||
'suspension_toggled' => '服务器状态已更改为 :status.',
|
||||
'rebuild_on_boot' => '此服务器已被标记为需要在Docker容器中启动。 此操作会在下次重启后生效。',
|
||||
'install_toggled' => '此服务器的安装状态已被更改',
|
||||
'server_reinstalled' => '此服务器目前已置于重装队列中,即将开始重装',
|
||||
'details_updated' => '服务器信息成功被更新',
|
||||
'docker_image_updated' => '成功更改用于该服务器的默认的Docker镜像。 此操作需要重启后生效',
|
||||
'node_required' => '你需要至少一个节点才能开始添加服务器',
|
||||
'startup_changed' => '已更新此服务器的启动配置。若此服务器的启动模板被更改,其将被重新安装。',
|
||||
'server_deleted' => '已成功从系统中删除服务器。',
|
||||
'server_created' => '已成功在面板中创建服务器。请稍等面板完全安装服务器完毕。',
|
||||
'build_updated' => '已更新此服务器的构建参数。部分更改可能需要重启才能生效。启动参数已更改。',
|
||||
'suspension_toggled' => '服务器停用状态已更改为 :status.',
|
||||
'rebuild_on_boot' => '此服务器已被标记为需要重新构建 Docker 容器。此操作会在下次启动服务器后生效。',
|
||||
'install_toggled' => '此服务器的安装状态已被更改。',
|
||||
'server_reinstalled' => '此服务器已置于即将开始的重装队列中。',
|
||||
'details_updated' => '已成功更新服务器信息。',
|
||||
'docker_image_updated' => '已成功更改此服务器使用的默认 Docker 镜像。此操作需要重启以应用更改。',
|
||||
'node_required' => '您需要配置至少一个节点以添加服务器至面板。',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
|
||||
return [
|
||||
'exceptions' => [
|
||||
'user_has_servers' => '无法删除一个拥有活动状态服务器的用户. 请在继续此操作前删除他的服务器',
|
||||
'user_has_servers' => '无法删除已绑定活跃服务器的账户。请删除服务器后继续。',
|
||||
],
|
||||
'notices' => [
|
||||
'account_created' => '成功创建用户',
|
||||
'account_updated' => '成功更新用户',
|
||||
'account_created' => '已成功创建用户。',
|
||||
'account_updated' => '已成功更新用户。',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
|
||||
return [
|
||||
'not_authorized' => '您无权执行此操作。',
|
||||
'auth_error' => '尝试登录时发生错误.',
|
||||
'authentication_required' => '需要认证才能继续操作',
|
||||
'auth_error' => '登录时发生错误。',
|
||||
'authentication_required' => '需要认证以继续',
|
||||
'remember_me' => '记住我',
|
||||
'sign_in' => '登陆',
|
||||
'forgot_password' => '忘记密码',
|
||||
'request_reset_text' => '忘记密码? 请在下方填入您的Email.',
|
||||
'reset_password_text' => '重设您账户的密码.',
|
||||
'request_reset_text' => '忘记密码了吗?请在下方填入您的电子邮件地址。',
|
||||
'reset_password_text' => '重设账户密码',
|
||||
'reset_password' => '重设密码',
|
||||
'email_sent' => '一封帮助您重置密码的电子邮件已发出,请查收并按提示操作(如未收到请检查垃圾箱)',
|
||||
'failed' => '用户名或密码错误, 或者两步验证失败.',
|
||||
'throttle' => '太多次登陆失败. 请在 :seconds 秒后尝试',
|
||||
'password_requirements' => '密码至少包含大写字母,小写字母,数字,并且在8位以上.',
|
||||
'email_sent' => '一封含有重置密码指引的邮件已发送至您的电子邮箱地址。',
|
||||
'failed' => '所提供的凭证与我们所记录的不符,或可能两步验证失败。',
|
||||
'throttle' => '登录尝试次数过多。请 :seconds 秒后重试。',
|
||||
'password_requirements' => '密码必须含有一位大写字母、小写字母及数字且长度至少为八位。',
|
||||
'request_reset' => '查找账户',
|
||||
'2fa_required' => '两步验证',
|
||||
'2fa_failed' => '两步验证密码错误',
|
||||
'totp_failed' => '错误的TOTP验证.',
|
||||
'2fa_must_be_enabled' => '管理员要求您的账户必须开启两步验证才能使用此面板.',
|
||||
'totp_failed' => '尝试进行两步验证时发生错误。',
|
||||
'2fa_must_be_enabled' => '管理员要求您启用两步验证才能使用面板。',
|
||||
];
|
||||
|
|
|
@ -3,86 +3,87 @@
|
|||
return [
|
||||
'validation_error' => '请求中有一个或多个字段出错',
|
||||
'errors' => [
|
||||
'return' => '返回上一个页面',
|
||||
'return' => '返回上页',
|
||||
'home' => '返回主页',
|
||||
'403' => [
|
||||
'header' => '禁止访问',
|
||||
'desc' => '您没有权限访问此服务器上的资源.',
|
||||
'desc' => '您没有访问此服务器上的资源的权限。',
|
||||
],
|
||||
'404' => [
|
||||
'header' => 'Not Found',
|
||||
'desc' => '未找到资源.',
|
||||
'header' => '文件未找到',
|
||||
'desc' => '我们无法在此服务器上找到所请求的资源。',
|
||||
],
|
||||
'installing' => [
|
||||
'header' => '服务器正在安装',
|
||||
'desc' => '请求的服务器仍然在部署中,请稍等几分钟,完成后您将收到一封电子邮件',
|
||||
'header' => '服务器安装中',
|
||||
'desc' => '请求的服务器正在完成安装进程。请几分钟后再来查看,您将在此过程完成后收到电子邮件提醒。',
|
||||
],
|
||||
'suspended' => [
|
||||
'header' => '服务器已暂停',
|
||||
'desc' => '此服务器已被暂停,无法访问,请联系管理员',
|
||||
'header' => '服务器已停用',
|
||||
'desc' => '此服务器已停用且无法访问。',
|
||||
],
|
||||
'maintenance' => [
|
||||
'header' => '节点维护中',
|
||||
'title' => '暂时不可用',
|
||||
'desc' => '此节点正在维护,当前无法访问.',
|
||||
'desc' => '此节点正在维护,当前无法访问。',
|
||||
],
|
||||
],
|
||||
'index' => [
|
||||
'header' => '您的服务器',
|
||||
'header_sub' => '您当前可访问的服务器.',
|
||||
'header_sub' => '您有权限访问的服务器。',
|
||||
'list' => '服务器列表',
|
||||
],
|
||||
'api' => [
|
||||
'index' => [
|
||||
'list' => '您的密钥',
|
||||
'header' => '账户 API',
|
||||
'header_sub' => '管理访问密钥允许您使用API操作面板.',
|
||||
'header_sub' => '管理允许您对面板执行操作的 API 密钥。',
|
||||
'create_new' => '新建 API 密钥',
|
||||
'keypair_created' => '新建API密钥成功.',
|
||||
'keypair_created' => '已成功生成 API 密钥并列于下方。',
|
||||
],
|
||||
'new' => [
|
||||
'header' => '新建 API 密钥',
|
||||
'header_sub' => '创建一个新的账户API密钥.',
|
||||
'form_title' => '选项',
|
||||
'header_sub' => '新建账户访问密钥。',
|
||||
'form_title' => '详细信息',
|
||||
'descriptive_memo' => [
|
||||
'title' => '描述',
|
||||
'description' => '添加一个关于此密钥的描述.',
|
||||
'description' => '请输入便于分辨此密钥的描述信息。',
|
||||
],
|
||||
'allowed_ips' => [
|
||||
'title' => '允许的IP',
|
||||
'description' => '添加IP地址限制来保护API安全. CIDR 标记是被允许的. 留空将允许所有IP.',
|
||||
'title' => '许可 IP',
|
||||
'description' => '输入允许使用此密钥的 IP 地址列表。此功能支持无类别域间路由。留空将允许所有 IP 使用。',
|
||||
],
|
||||
],
|
||||
],
|
||||
'account' => [
|
||||
'details_updated' => '您账户的信息成功更新.',
|
||||
'invalid_password' => '您提供的密码不正确.',
|
||||
'details_updated' => '已成功更新您的账户信息。',
|
||||
'invalid_password' => '您提供的密码无效。',
|
||||
'header' => '您的账户',
|
||||
'header_sub' => '管理您的账户信息.',
|
||||
'update_pass' => '修改密码',
|
||||
'update_email' => '修改 Email 地址',
|
||||
'update_email' => '修改电子邮件地址',
|
||||
'current_password' => '当前密码',
|
||||
'new_password' => '新密码',
|
||||
'new_password_again' => '重复密码',
|
||||
'new_email' => '新 Email 地址',
|
||||
'first_name' => '姓',
|
||||
'last_name' => '名',
|
||||
'new_email' => '新电子邮件地址',
|
||||
'first_name' => '姓氏',
|
||||
'last_name' => '名称',
|
||||
'update_identity' => '更新个人信息',
|
||||
'username_help' => '您的用户名必须唯一(未被使用),并满足以下要求: :requirements.',
|
||||
'username_help' => '您的用户名必须未被他人使用,且仅包含下列字符::requirements。',
|
||||
'language' => '语言',
|
||||
],
|
||||
'security' => [
|
||||
'session_mgmt_disabled' => '为了安全原因,您的此次会话无法访问用户管理.',
|
||||
'session_mgmt_disabled' => '您的托管商未启用此界面来管理账户会话。',
|
||||
'header' => '账户安全',
|
||||
'header_sub' => '管理活动会话和两步认证.',
|
||||
'sessions' => '活动中的会话',
|
||||
'header_sub' => '管理活跃中的会话与两步验证。',
|
||||
'sessions' => '活跃会话',
|
||||
'2fa_header' => '两步验证',
|
||||
'2fa_token_help' => '填入您两步验证生成器生成的密码 (Google Authenticator, Authy, etc.).',
|
||||
'2fa_token_help' => '请填入由应用程序所生成的两步验证密钥(Google 身份验证器、Authy 等)。',
|
||||
'disable_2fa' => '关闭两步验证',
|
||||
'2fa_enabled' => '两步验证已开启,在您登陆面板时会要求两步验证.如果您想关闭两步验证,只需输入两步验证的密码即可',
|
||||
'2fa_disabled' => '两步验证已关闭! 您应该开启两步验证将其作为您账户的额外防护',
|
||||
'enable_2fa' => '开启两步验证',
|
||||
'2fa_qr' => '在您的设备上上配置两步验证',
|
||||
'2fa_checkpoint_help' => '使用两步验证需要用您的应用扫左侧二维码, 或手动输入下方的代码.完成后请将生成的密码输入下方方框.',
|
||||
'2fa_disable_error' => '两步验证密码错误. 关闭两步验证失败.',
|
||||
'2fa_enabled' => '已为此账户启用两步验证,您将需要验证以登录至此账户。若您想关闭两步验证,您只需在下方输入密钥并提交即可。',
|
||||
'2fa_disabled' => '已关闭两步验证!您应启用此功能来作为此账户的附加防护手段。',
|
||||
'enable_2fa' => '启用两步验证',
|
||||
'2fa_qr' => '在您的设备上配置两步验证',
|
||||
'2fa_checkpoint_help' => '在您的手机上使用两步验证应用程序扫描左侧的二维码或直接输入下方的代码。录入后,请在下方输入应用程序生成的密码。',
|
||||
'2fa_disable_error' => '提供的两步验证密钥无效。未关闭此账户的两步验证。两步验证密码错误. 关闭两步验证失败.',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -2,54 +2,54 @@
|
|||
|
||||
return [
|
||||
'key' => [
|
||||
'warning' => '貌似您已经拥有一个应用加密密钥了. 继续操作会导致之前的密钥被覆盖,所有的加密文件都将损坏。 !!!危险操作,请注意文件安全!!!',
|
||||
'confirm' => '我已了解此操作的后果,可以承受丢失文件的风险,请继续。',
|
||||
'final_confirm' => '确定继续操作? 更改应用加密密钥 !!将会导致数据丢失!!.',
|
||||
'warning' => '似乎您已配置应用程序加密密钥。继续此操作将覆盖密钥并损坏已加密数据。请了解您所执行的操作后再决定是否继续!!!',
|
||||
'confirm' => '我已了解执行此操作的后果并承受丢失加密数据的风险,请继续。',
|
||||
'final_confirm' => '确您是否决定继续?更改应用程序加密密钥将导致数据丢失!!!',
|
||||
],
|
||||
'location' => [
|
||||
'no_location_found' => '可用区ID错误:无发找到该可用区',
|
||||
'ask_short' => '可用区ID',
|
||||
'ask_long' => '可用区描述',
|
||||
'created' => '成功创建可用区 (:name) ,可用区ID: :id.',
|
||||
'deleted' => '成功删除指定的可用区。',
|
||||
'no_location_found' => '无法找到与提供的代码匹配的记录。',
|
||||
'ask_short' => '地区代码',
|
||||
'ask_long' => '地区描述',
|
||||
'created' => '已成功创建地区(:name),编号为 :id。',
|
||||
'deleted' => '已成功删除请求的地区。',
|
||||
],
|
||||
'user' => [
|
||||
'search_users' => '输入用户名, UUID, 或 Email 地址',
|
||||
'select_search_user' => '要删除的用户ID (键入 \'0\' 来重新搜索)',
|
||||
'deleted' => '成功删除用户。',
|
||||
'confirm_delete' => '确定要删除此用户吗',
|
||||
'no_users_found' => '未找到指定的用户',
|
||||
'multiple_found' => '找到多个用户, 无法删除用户,原因: --no-interaction 参数。',
|
||||
'ask_admin' => '此用户是管理员吗?',
|
||||
'search_users' => '请输入用户名、UUID 或电子邮件地址',
|
||||
'select_search_user' => '待删除的用户编号(请键入 \'0\' 来重新搜索)',
|
||||
'deleted' => '已成功从面板删除用户。',
|
||||
'confirm_delete' => '您是否想从面板中删除此用户?',
|
||||
'no_users_found' => '未找到匹配搜索项的用户。',
|
||||
'multiple_found' => '已找到多个匹配搜索项的用户,由于 --no-interaction 参数而无法删除。',
|
||||
'ask_admin' => '此用户是否为管理员?',
|
||||
'ask_email' => '电子邮件地址',
|
||||
'ask_username' => '用户名',
|
||||
'ask_name_first' => '姓',
|
||||
'ask_name_last' => '名',
|
||||
'ask_name_first' => '姓氏',
|
||||
'ask_name_last' => '名称',
|
||||
'ask_password' => '密码',
|
||||
'ask_password_tip' => '如果您想创建一个随机密码的用户,请重新执行指令(CTRL+C) 并输入 `--no-password` 参数.',
|
||||
'ask_password_help' => '密码至少8位,并包含一个字母和数字',
|
||||
'ask_password_tip' => '若您想创建用户并稍后发送生成的随机密码给用户,请重新运行此命令(CTRL+C)并添加 `--no-password` 参数。',
|
||||
'ask_password_help' => '密码长度必须至少为八位且包含至少一位大写字母和数字。',
|
||||
'2fa_help_text' => [
|
||||
'此命令会关闭一个用户的两步验证(如果他打开了). 此命令应仅用于用户恢复或解锁(两步验证无法成功情况下)。',
|
||||
'如果您不想这么做, 情书用 CTRL+C 退出此操作。',
|
||||
'此命令将关闭账户的两步验证(若启用)。此命令应作为账户被锁定时的恢复措施。',
|
||||
'若您不想这么做,请使用 CTRL+C 退出进程。',
|
||||
],
|
||||
'2fa_disabled' => '已成功禁用以下账户的两步验证: :email.',
|
||||
'2fa_disabled' => '已成功为 :email 禁用两步验证。',
|
||||
],
|
||||
'schedule' => [
|
||||
'output_line' => '第一次任务已计划于 `:schedule` (:hash).',
|
||||
'output_line' => '首次任务将于 `:schedule`(:hash)后执行。',
|
||||
],
|
||||
'maintenance' => [
|
||||
'deleting_service_backup' => '正在删除服务备份文件 :file.',
|
||||
'deleting_service_backup' => '正在删除服务备份文件 :file。',
|
||||
],
|
||||
'server' => [
|
||||
'rebuild_failed' => '重构操作 ":name" (#:id) ,位于节点 ":node" 失败,错误信息: :message',
|
||||
'rebuild_failed' => '节点 ":node" 上的重构操作 ":name"(#:id) 发生了 :message 错误。',
|
||||
'power' => [
|
||||
'confirm' => '您即将执行 :action 在 :count 个服务器. 是否继续?',
|
||||
'action_failed' => '电源命令 ":name" (#:id) 位于节点 ":node" 失败,错误信息: :message',
|
||||
'confirm' => '您将在 :count 台服务器上执行 :action 操作。是否继续?',
|
||||
'action_failed' => '节点 ":node" 上的电源命令 ":name"(#:id) 发生了 :message 错误。',
|
||||
],
|
||||
],
|
||||
'environment' => [
|
||||
'mail' => [
|
||||
'ask_smtp_host' => 'SMTP 主机 (e.g. smtp.gmail.com)',
|
||||
'ask_smtp_host' => 'SMTP 主机(如 smtp.gmail.com)',
|
||||
'ask_smtp_port' => 'SMTP 端口',
|
||||
'ask_smtp_username' => 'SMTP 用户名',
|
||||
'ask_smtp_password' => 'SMTP 密码',
|
||||
|
@ -57,41 +57,41 @@ return [
|
|||
'ask_mailgun_secret' => 'Mailgun 密钥',
|
||||
'ask_mandrill_secret' => 'Mandrill 密钥',
|
||||
'ask_postmark_username' => 'Postmark API 密钥',
|
||||
'ask_driver' => '哪个引擎应该用于发送邮件?',
|
||||
'ask_mail_from' => 'Email来自哪个邮箱',
|
||||
'ask_mail_name' => 'Email应该由谁发送(发送者姓名)?',
|
||||
'ask_encryption' => '应该使用的加密方法',
|
||||
'ask_driver' => '应使用哪款引擎发送邮件?',
|
||||
'ask_mail_from' => '电子邮件地址的邮件发信人为',
|
||||
'ask_mail_name' => '电子邮件的显示发信人为',
|
||||
'ask_encryption' => '加密方法',
|
||||
],
|
||||
'database' => [
|
||||
'host_warning' => '极度推荐不使用localhost作为主机地址(可能有bug). 如果确实要使用本机作为MySQL地址,请使用 "127.0.0.1".',
|
||||
'host_warning' => '由于经常发生套接字连接错误,我们极度不推荐您使用 “localhost” 作为数据库主机地址。若您仍想使用本地连接则应使用 “127.0.0.1”。',
|
||||
'host' => '数据库主机',
|
||||
'port' => '数据库端口',
|
||||
'database' => '数据库名',
|
||||
'username_warning' => '使用 "root" 账户会导致安全漏洞, 翼龙面板不允许此账户作为面板数据库账户. 你应该为此程序创建MySQL庄户',
|
||||
'username_warning' => '不仅翼龙面板不允许使用 "root" 账户连接 MySQL 数据库,且这将产生严重安全漏洞。您应为此软件单独创建 MySQL 账户。',
|
||||
'username' => '数据库用户名',
|
||||
'password_defined' => '您似乎已经指定了MySQL连接密码,你想更改它吗',
|
||||
'password_defined' => '您似乎已创建了带有密码的 MySQL 连接,您是否想更改?',
|
||||
'password' => '数据库密码',
|
||||
'connection_error' => '无法连接数据库. 返回错误: ":error".',
|
||||
'creds_not_saved' => '您的数据库访问信息未保存. 在继续之前您将需要提供有效的信息',
|
||||
'try_again' => '返回再试一次?',
|
||||
'connection_error' => '无法使用提供的凭证连接 MySQL 服务器。 返回的错误为 ":error"。',
|
||||
'creds_not_saved' => '您的数据库访问凭证尚未保存。您需要在继续前提供有效的连接信息。',
|
||||
'try_again' => '是否返回重试?',
|
||||
],
|
||||
'app' => [
|
||||
'settings' => '启用基于UI的设置编辑器?',
|
||||
'author' => '管理模板作者Email',
|
||||
'author_help' => '提供此面板到处的管理模板作者的电子邮件地址. 这应该是一个合法的电子邮件地址',
|
||||
'app_url_help' => '这个应用的URL必须以 https:// or http:// 开头(取决于是否启用SSL). 如果不包含这些您的电子邮件地址和其他内容可能会指向错误的地址.',
|
||||
'app_url' => '应用 URL',
|
||||
'timezone_help' => '设置的时区应该满足PHP支持的时区. 如果您不确定,请参阅 http://php.net/manual/en/timezones.php.',
|
||||
'timezone' => '应用时区',
|
||||
'cache_driver' => 'Cache Driver',
|
||||
'session_driver' => 'Session Driver',
|
||||
'queue_driver' => 'Queue Driver',
|
||||
'using_redis' => '如果您选择使用Redis, 请在下方提供有效的连接信息. 一般使用默认信息即可,除非您更改过设置.',
|
||||
'settings' => '是否启用可视化设置编辑器?',
|
||||
'author' => '管理模板作者电子邮件地址',
|
||||
'author_help' => '提供从此面板导出管理模板人员的电子邮件地址。此电子邮件地址必须合法。',
|
||||
'app_url_help' => '根据您是否启用 SSL 来决定此应用程序的 URL 应为 https:// 或 http://。若您选择错误,您的电子邮件及其他内容将指向到错误地址。',
|
||||
'app_url' => '应用程序 URL',
|
||||
'timezone_help' => '时区应匹配 PHP 所支持的时区。 如果您不确定,请参阅 http://php.net/manual/en/timezones.php。',
|
||||
'timezone' => '应用程序时区',
|
||||
'cache_driver' => '缓存驱动程序',
|
||||
'session_driver' => '会话驱动程序',
|
||||
'queue_driver' => '队列驱动程序',
|
||||
'using_redis' => '若您选择使用 Redis,请在下方提供有效的连接信息。在您未更改设置的大多数情况下,您均可使用默认值。',
|
||||
'redis_host' => 'Redis 主机',
|
||||
'redis_password' => 'Redis 密码',
|
||||
'redis_pass_help' => '默认情况下,Redis数据库不需要密码,且仅运行于本地. 这种情况下,您什么都不用填.',
|
||||
'redis_pass_help' => '默认情况下,Redis 服务器实例无需密码且在本地运行禁止外界访问。这种情况下,您只需回车即可。',
|
||||
'redis_port' => 'Redis 端口',
|
||||
'redis_pass_defined' => '似乎您已经设置过Redis密码了,您需要更改吗?',
|
||||
'redis_pass_defined' => '您似乎已为 Redis 配置了密码,您是否想更改?',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -1,68 +1,68 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'daemon_connection_failed' => '连接受控端时发生意外 返回错误码 HTTP/:code response code. 此错误已被记录',
|
||||
'daemon_connection_failed' => '尝试连接守护程序是发生错误,状态码 HTTP/:code。此错误已被记录。',
|
||||
'node' => [
|
||||
'servers_attached' => '节点删除必须按先移除其所有的服务器.',
|
||||
'daemon_off_config_updated' => '受控端配置 <strong>已被更新</strong>, 但是自动更新受控端上的配置文件时发生错误. 你需要手动将配置文件 (core.json) 更新至受控端来完成更新.',
|
||||
'servers_attached' => '要删除节点,您必须先取消其与其他服务器的关联。',
|
||||
'daemon_off_config_updated' => '<strong>已更新</strong>守护程序配置,但在尝试自动更新守护程序配置文件时发生错误。您需要手动更新守护程序的配置文件(core.json)以应用更改。',
|
||||
],
|
||||
'allocations' => [
|
||||
'server_using' => '一个服务器已分配该地址. 一个地址只有在无服务器使用时才能删除.',
|
||||
'too_many_ports' => '一次添加1000个以上的端口是不被支持的.',
|
||||
'invalid_mapping' => '提供的端口: :port 无效,无法继续操作.',
|
||||
'cidr_out_of_range' => 'CIDR 标记 只允许掩码在 /25 到 /32之间。',
|
||||
'port_out_of_range' => '端口超过范围,范围必须在 1024 到 65535 之间.',
|
||||
'server_using' => '已有服务器被分配到该地址。您必须先解除关联才能删除此地址。',
|
||||
'too_many_ports' => '不支持单次添加多于 1000 个端口。',
|
||||
'invalid_mapping' => '所提供的端口 :port 无效,无法继续操作。',
|
||||
'cidr_out_of_range' => '类别域间路由仅允许介于 /25 和 /32 之间的掩码。',
|
||||
'port_out_of_range' => '分配端口的范围必须介于 1024 至 65535 之间。',
|
||||
],
|
||||
'nest' => [
|
||||
'delete_has_servers' => '活动服务器使用的管理模块不能被删除.',
|
||||
'delete_has_servers' => '无法删除附着到活跃服务器上的管理模块。',
|
||||
'egg' => [
|
||||
'delete_has_servers' => '活动服务器使用的管理模板不能被删除.',
|
||||
'invalid_copy_id' => '管理模板复制的脚本ID无效.',
|
||||
'must_be_child' => ' "复制设置自"选项指定的目标必须是管理模块的附属.',
|
||||
'has_children' => '此管理模版附属有一个或多个管理模板. 在删除之前请先删除所有附属.',
|
||||
'delete_has_servers' => '无法删除附着到活跃服务器上的管理模块。',
|
||||
'invalid_copy_id' => '用于复制脚本的管理模板不存在,或脚本本身不存在。',
|
||||
'must_be_child' => '“复制设置自”选项指定的目标必须为所选管理模块的子选项。',
|
||||
'has_children' => '此管理模版为一个或多个管理模板的母模板。请先删除其他管理模板再删除此模板。',
|
||||
],
|
||||
'variables' => [
|
||||
'env_not_unique' => '环境变量 :name 必须唯一.',
|
||||
'reserved_name' => '环境变量 :name 是被保护的,无法指定为变量.',
|
||||
'bad_validation_rule' => '环境变量规则 ":rule" 对于这个应用不是一个有效的规则.',
|
||||
'env_not_unique' => '此管理面板的环境变量 :name 必须唯一。',
|
||||
'reserved_name' => '环境变量 :name 被保护的且无法被分配至其他变量。',
|
||||
'bad_validation_rule' => '验证规则 “:rule” 不是此应用程序的有效规则。',
|
||||
],
|
||||
'importer' => [
|
||||
'json_error' => '尝试导入JSON 文件时发生错误: :error.',
|
||||
'file_error' => '提供的JSON文件不合法.',
|
||||
'invalid_json_provided' => '提供的JSON文件格式不正确,无法被解析。',
|
||||
'json_error' => '导入 JSON 文件时发生错误::error.',
|
||||
'file_error' => '所提供的 JSON 文件无效。',
|
||||
'invalid_json_provided' => '所提供的 JSON 文件格式无法被解析。',
|
||||
],
|
||||
],
|
||||
'packs' => [
|
||||
'delete_has_servers' => '活动服务器使用的整合包不能被删除',
|
||||
'update_has_servers' => '当前有服务器附属于包时无法修改关联选项的ID.',
|
||||
'invalid_upload' => '上传的文件不合法.',
|
||||
'invalid_mime' => '上传的文件不符合要求的文件类型 :type',
|
||||
'unreadable' => '服务器无法打开该压缩包.',
|
||||
'zip_extraction' => '解压时发生错误.',
|
||||
'invalid_archive_exception' => '压缩包缺失archive.tar.gz 或 import.json 文件在根目录.',
|
||||
'delete_has_servers' => '无法删除依附到活跃服务器的整合包。',
|
||||
'update_has_servers' => '无法在有服务器依附至整合包时修改关联选项编号。',
|
||||
'invalid_upload' => '所提供的文件格式无效。',
|
||||
'invalid_mime' => '提供的文件不符合所需文件类型 :type',
|
||||
'unreadable' => '服务器无法打开所提供的归档文件。',
|
||||
'zip_extraction' => '提取归档文件至服务器时发生错误。',
|
||||
'invalid_archive_exception' => '整合包归档文件的根目录似乎缺少 archive.tar.gz 或 import.json。',
|
||||
],
|
||||
'subusers' => [
|
||||
'editing_self' => '编辑您自己的子用户时不被允许的.',
|
||||
'user_is_owner' => '子用户无法添加服主.',
|
||||
'subuser_exists' => '那个电子邮件的用户已经是此服务器的子用户了.',
|
||||
'editing_self' => '您无法作为子用户编辑您自己的子用户账号。',
|
||||
'user_is_owner' => '您无法作为子用户来添加为此服务器的服主。',
|
||||
'subuser_exists' => '使用该电子邮件地址的用户已被分配为此服务器的子用户。',
|
||||
],
|
||||
'databases' => [
|
||||
'delete_has_databases' => '无法删除一个拥有活跃数据库的数据库服务器.',
|
||||
'delete_has_databases' => '无法删除关联至活跃数据库的数据库服务器。',
|
||||
],
|
||||
'tasks' => [
|
||||
'chain_interval_too_long' => '链接任务的最大间隔时间为15分钟。',
|
||||
'chain_interval_too_long' => '连环任务的最大时间间隔为 15 分钟。',
|
||||
],
|
||||
'locations' => [
|
||||
'has_nodes' => '活动节点附属的可用区无法被删除.',
|
||||
'has_nodes' => '无法删除被依附活动节点的区域。',
|
||||
],
|
||||
'users' => [
|
||||
'node_revocation_failed' => '吊销密钥失败 <a href=":link">节点 #:node</a>. :error',
|
||||
'node_revocation_failed' => '注销<a href=":link">节点 #:node</a> 的密钥失败::error',
|
||||
],
|
||||
'deployment' => [
|
||||
'no_viable_nodes' => '没有合适的节点来自动部署服务器',
|
||||
'no_viable_allocations' => '没有合适的地址来自动部署服务器',
|
||||
'no_viable_nodes' => '无法找到满足自动化部署需求的节点。',
|
||||
'no_viable_allocations' => '无法找到满足自动化部署需求的分配地址。',
|
||||
],
|
||||
'api' => [
|
||||
'resource_not_found' => '需求的资源未找到.',
|
||||
'resource_not_found' => '服务器上不存在所请求的资源。',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'home' => '主页',
|
||||
'home' => '首页',
|
||||
'account' => [
|
||||
'header' => '账户管理',
|
||||
'my_account' => '我的账户',
|
||||
|
|
|
@ -12,6 +12,6 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'previous' => '« 上一步',
|
||||
'next' => '下一步 »',
|
||||
'previous' => '« 上页',
|
||||
'next' => '下页 »',
|
||||
];
|
||||
|
|
|
@ -11,9 +11,9 @@ return [
|
|||
| has failed, such as for an invalid token or invalid new password.
|
||||
|
|
||||
*/
|
||||
'password' => '密码至少六位数,并且两次输入的密码一致.',
|
||||
'reset' => '您的密码已重设!',
|
||||
'sent' => '我们已发送密码重设电子邮件!',
|
||||
'token' => '此密码重设连接的令牌已过期.',
|
||||
'user' => '无法找到此Email的用户.',
|
||||
'password' => '密码必须至少包含六位字符且与确认密码匹配。',
|
||||
'reset' => '已重设您的密码!',
|
||||
'sent' => '我们已发送密码重设链接至您的电子邮件地址!',
|
||||
'token' => '此密码重设令牌无效。',
|
||||
'user' => '我们无法找到使用此电子邮件地址的用户。',
|
||||
];
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
|
||||
return [
|
||||
'index' => [
|
||||
'title' => '服务器状态 :name',
|
||||
'title' => '查看服务器 :name ',
|
||||
'header' => '服务器控制台',
|
||||
'header_sub' => '实时控制您的服务器.',
|
||||
'header_sub' => '实时掌控您的服务器。',
|
||||
],
|
||||
'schedule' => [
|
||||
'header' => '计划任务',
|
||||
'header_sub' => '在一处,轻松管理服务器任务.',
|
||||
'header_sub' => '一处轻松掌管服务器任务。',
|
||||
'current' => '当前计划',
|
||||
'new' => [
|
||||
'header' => '创建新任务',
|
||||
'header_sub' => '创建一个新的定时任务.',
|
||||
'header' => '新建计划',
|
||||
'header_sub' => '为此服务器新建一组计划任务。',
|
||||
'submit' => '创建任务',
|
||||
],
|
||||
'manage' => [
|
||||
|
@ -21,63 +21,63 @@ return [
|
|||
'delete' => '删除任务',
|
||||
],
|
||||
'task' => [
|
||||
'time' => '在。。。之后',
|
||||
'time' => '在···之后',
|
||||
'action' => '执行操作',
|
||||
'payload' => '任务内容',
|
||||
'add_more' => '添加另一个任务',
|
||||
'add_more' => '添加其他任务',
|
||||
],
|
||||
'actions' => [
|
||||
'command' => '发送命令',
|
||||
'power' => '电源命令',
|
||||
],
|
||||
'toggle' => '更改状态',
|
||||
'run_now' => '触发任务(现在)',
|
||||
'schedule_created' => '成功在服务器上创建一个计划任务.',
|
||||
'schedule_updated' => '任务已被更新.',
|
||||
'run_now' => '触发任务',
|
||||
'schedule_created' => '已成功在此服务器上新建计划任务。',
|
||||
'schedule_updated' => '已更新计划任务。',
|
||||
'unnamed' => '未命名任务',
|
||||
'setup' => '任务创建',
|
||||
'setup' => '任务配置',
|
||||
'day_of_week' => '星期',
|
||||
'day_of_month' => '日',
|
||||
'hour' => '小时',
|
||||
'minute' => '分钟',
|
||||
'time_help' => '任务系统在定义任务应该何时开始运行时支持使用Cronjob语法。 使用上面的字段指定何时应开始运行这些任务,或从多个选择菜单中选择选项。',
|
||||
'task_help' => '任务的时间与先前定义的任务相关。 每个计划任务可能分配的任务不超过5个,任务可能不会超过15分钟的时间安排。',
|
||||
'hour' => '时',
|
||||
'minute' => '分',
|
||||
'time_help' => '计划任务系统支持使用 Cronjob 语法来定义任务启动时间。使用上方的字段来指定计划任务的开始时间或选择多选菜单中的多个选项。',
|
||||
'task_help' => '任务时间与先前定义的任务紧密相关。每个计划最多可分配 5 项任务且任务时间间隔不得超过 15 分钟。',
|
||||
],
|
||||
'tasks' => [
|
||||
'task_created' => '成功在面板上创建一个新任务',
|
||||
'task_updated' => '任务成功被更新. 列表中的所有任务会被取消,并在下一次设定的时间运行.',
|
||||
'header' => '计划的任务',
|
||||
'header_sub' => '自动化你的服务器.',
|
||||
'current' => '当前计划的任务',
|
||||
'task_created' => '已成功在面板上新建任务。',
|
||||
'task_updated' => '已成功更新任务。现有的所有队列中的任务操作将被取消并于下个定义时间执行。',
|
||||
'header' => '计划任务',
|
||||
'header_sub' => '自动化您的服务器。',
|
||||
'current' => '当前计划任务',
|
||||
'actions' => [
|
||||
'command' => '发送命令',
|
||||
'power' => '发送电源命令',
|
||||
'power' => '发送电源指令',
|
||||
],
|
||||
'new_task' => '添加新任务',
|
||||
'new_task' => '新增任务',
|
||||
'toggle' => '更改状态',
|
||||
'new' => [
|
||||
'header' => '新任务',
|
||||
'header_sub' => '为这个服务器创建一个新任务。',
|
||||
'task_name' => '任务名',
|
||||
'header' => '新建任务',
|
||||
'header_sub' => '为此服务器新建计划任务。',
|
||||
'task_name' => '任务名称',
|
||||
'day_of_week' => '星期',
|
||||
'custom' => '自定义',
|
||||
'day_of_month' => '日',
|
||||
'hour' => '小时',
|
||||
'hour' => '时',
|
||||
'minute' => '分',
|
||||
'sun' => '周日',
|
||||
'mon' => '周一',
|
||||
'tues' => '周二',
|
||||
'wed' => '周三',
|
||||
'thurs' => '周四',
|
||||
'fri' => '周五',
|
||||
'sat' => '周六',
|
||||
'sun' => '星期日',
|
||||
'mon' => '星期一',
|
||||
'tues' => '星期二',
|
||||
'wed' => '星期三',
|
||||
'thurs' => '星期四',
|
||||
'fri' => '星期五',
|
||||
'sat' => '星期六',
|
||||
'submit' => '创建任务',
|
||||
'type' => '任务类型',
|
||||
'chain_then' => '然后, 之后',
|
||||
'chain_then' => '先···再···',
|
||||
'chain_do' => '执行',
|
||||
'chain_arguments' => '参数',
|
||||
'chain_arguments' => '使用参数',
|
||||
'payload' => '任务内容',
|
||||
'payload_help' => '例如, 如果你选择 <code>发送命令</code> ,就在此处填写要发送的命令. 如果您选择 <code>发送电源命令</code> 在这里填入电源命令 (e.g. <code>restart</code>).',
|
||||
'payload_help' => '例如,若您选择<code>发送命令</code>,请在此处填写要发送的命令。若您选择<code>发送电源指令</code>,请在此处填写电源命令(如<code>重启</code>).',
|
||||
],
|
||||
'edit' => [
|
||||
'header' => '任务管理',
|
||||
|
@ -86,22 +86,22 @@ return [
|
|||
],
|
||||
'users' => [
|
||||
'header' => '用户管理',
|
||||
'header_sub' => '控制访问你服务器的用户.',
|
||||
'header_sub' => '掌控谁能访问您的服务器。',
|
||||
'configure' => '配置权限',
|
||||
'list' => '有权限的用户列表',
|
||||
'add' => '添加一个新的子用户',
|
||||
'update' => '修改子用户',
|
||||
'user_assigned' => '成功连接到一个子用户连接到服务器.',
|
||||
'user_updated' => '成功更新权限.',
|
||||
'list' => '权限用户',
|
||||
'add' => '新增子用户',
|
||||
'update' => '更新子用户',
|
||||
'user_assigned' => '已成功分配新子用户至此服务器。',
|
||||
'user_updated' => '已成功更新权限。',
|
||||
'edit' => [
|
||||
'header' => '编辑子用户',
|
||||
'header_sub' => '管理用户在此服务器的访问权限.',
|
||||
'header_sub' => '编辑此用户的服务器访问权限。',
|
||||
],
|
||||
'new' => [
|
||||
'header' => '添加新用户',
|
||||
'header_sub' => '添加一个允许访问此服务器的用户.',
|
||||
'email' => 'Email 地址',
|
||||
'email_help' => '填入你希望邀请的协助您管理服务器的人的Email地址.',
|
||||
'header' => '新增新用户',
|
||||
'header_sub' => '新增允许访问此服务器的用户。',
|
||||
'email' => '电子邮件地址',
|
||||
'email_help' => '输入您邀请管理此服务器用户的电子邮件地址。',
|
||||
'power_header' => '电源管理',
|
||||
'file_header' => '文件管理',
|
||||
'subuser_header' => '子用户管理',
|
||||
|
@ -110,180 +110,184 @@ return [
|
|||
'database_header' => '数据库管理',
|
||||
'power_start' => [
|
||||
'title' => '启动服务器',
|
||||
'description' => '允许该用户启动服务器.',
|
||||
'description' => '允许此用户启动服务器。',
|
||||
],
|
||||
'power_stop' => [
|
||||
'title' => '停止服务器',
|
||||
'description' => '允许该用户停止服务器.',
|
||||
'description' => '允许此用户停止服务器。',
|
||||
],
|
||||
'power_restart' => [
|
||||
'title' => '重新启动服务器',
|
||||
'description' => '允许该用户重新启动服务器',
|
||||
'description' => '允许此用户重新启动服务器。',
|
||||
],
|
||||
'power_kill' => [
|
||||
'title' => '强制结束服务器',
|
||||
'description' => '允许该用户强行关闭服务器',
|
||||
'title' => '强制关闭服务器',
|
||||
'description' => '允许此用户强行关闭服务器。',
|
||||
],
|
||||
'send_command' => [
|
||||
'title' => '发送控制台命令',
|
||||
'description' => '允许用户发送控制台. 如果用户没有"停止服务器"权限,那么他无法使用stop命令',
|
||||
'description' => '允许用户发送控制台命令。若用户没有“停止服务器”权限,则其 stop 命令。',
|
||||
],
|
||||
'access_sftp' => [
|
||||
'title' => 'SFTP 权限',
|
||||
'description' => '允许用户连接到受控端提供的SFTP服务器.',
|
||||
'description' => '允许用户连接到守护程序所提供的 SFTP 服务器。',
|
||||
],
|
||||
'list_files' => [
|
||||
'title' => '列出文件',
|
||||
'description' => '允许用户列出所有文件及文件夹列表,但是无权访问文件.',
|
||||
'description' => '允许用户列出服务器上所有文件及文件夹,但是无法查看文件内容。',
|
||||
],
|
||||
'edit_files' => [
|
||||
'title' => '编辑文件',
|
||||
'description' => '允许用户访问文件内容(但更改后无法保存). SFTP 不受此权限影响.',
|
||||
'description' => '允许用户打开文件查看内容。SFTP 不受此权限影响。',
|
||||
],
|
||||
'save_files' => [
|
||||
'title' => 'Save Files',
|
||||
'description' => '允许用户保存文件(和编辑文件权限联动). SFTP 不受此权限影响.',
|
||||
'title' => '保存文件',
|
||||
'description' => '允许用户保存编辑过的文件内容。SFTP 不受此权限影响。',
|
||||
],
|
||||
'move_files' => [
|
||||
'title' => '重命名和移动文件',
|
||||
'description' => '允许用户在文件系统中重命名和移动文件及文件夹.',
|
||||
'title' => '重命名与移动文件',
|
||||
'description' => '允许用户在文件系统上重命名与移动文件及文件夹。',
|
||||
],
|
||||
'copy_files' => [
|
||||
'title' => '复制文件',
|
||||
'description' => '允许用户在文件系统中复制文件及文件夹.',
|
||||
'description' => '允许用户在文件系统上复制文件及文件夹。',
|
||||
],
|
||||
'compress_files' => [
|
||||
'title' => '压缩文件',
|
||||
'description' => '允许用户在文件系统中压缩文件及文件夹',
|
||||
'description' => '允许用户在文件系统上压缩文件及文件夹。',
|
||||
],
|
||||
'decompress_files' => [
|
||||
'title' => '解压文件',
|
||||
'description' => '允许用户解压 .zip 和 .tar(.gz) 压缩文件.',
|
||||
'description' => '允许用户解压 .zip 和 .tar(.gz)归档文件。',
|
||||
],
|
||||
'create_files' => [
|
||||
'title' => '创建文件',
|
||||
'description' => '允许用户通过面板创建文件.',
|
||||
'description' => '允许用户通过面板创建文件。',
|
||||
],
|
||||
'upload_files' => [
|
||||
'title' => '上传文件',
|
||||
'description' => '允许用户通过文件管理上传文件.',
|
||||
'description' => '允许用户通过文件管理上传文件。',
|
||||
],
|
||||
'delete_files' => [
|
||||
'title' => '删除文件',
|
||||
'description' => '允许用户在文件系统中删除文件.',
|
||||
'description' => '允许用户删除文件系统上的文件。',
|
||||
],
|
||||
'download_files' => [
|
||||
'title' => '下载文件s',
|
||||
'description' => '允许用户下载文件. 如果为用户分配该权限,那么他将自动拥有下载和查看文件内容的权限.',
|
||||
'title' => '下载文件',
|
||||
'description' => '允许用户下载文件。若用户被给予此权限,其可以在下载后查看文件而无需所需面板权限。',
|
||||
],
|
||||
'list_subusers' => [
|
||||
'title' => '列出子用户',
|
||||
'description' => '允许用户访问此服务器的子用户列表.',
|
||||
'description' => '允许用户访问此服务器的子用户列表。',
|
||||
],
|
||||
'view_subuser' => [
|
||||
'title' => '访问子用户',
|
||||
'description' => '允许用户查看子用户的权限.',
|
||||
'title' => '查看子用户',
|
||||
'description' => '允许用户查看子用户的权限。',
|
||||
],
|
||||
'edit_subuser' => [
|
||||
'title' => '编辑子用户',
|
||||
'description' => '允许用户编辑此服务器上的子用户权限.',
|
||||
'description' => '允许用户编辑此服务器上的子用户权限。',
|
||||
],
|
||||
'create_subuser' => [
|
||||
'title' => '创建子用户',
|
||||
'description' => '允许用户在此服务器上添加子用户.',
|
||||
'description' => '允许用户在此服务器上添加子用户。',
|
||||
],
|
||||
'delete_subuser' => [
|
||||
'title' => '删除子用户',
|
||||
'description' => '允许用户删除此服务器上的子用户.',
|
||||
'description' => '允许用户删除此服务器上的子用户。',
|
||||
],
|
||||
'view_allocations' => [
|
||||
'title' => '访问分配表',
|
||||
'description' => '允许用户访问所有分配到此服务器上的IP和端口列表.',
|
||||
'title' => '查看分配',
|
||||
'description' => '允许用户查看所有分配到此服务器上的 IP 及端口。',
|
||||
],
|
||||
'edit_allocation' => [
|
||||
'title' => '编辑默认连接',
|
||||
'description' => '允许用户更改连接到此服务器的默认连接地址.',
|
||||
'description' => '允许用户更改此服务器的默认连接地址。',
|
||||
],
|
||||
'view_startup' => [
|
||||
'title' => '访问启动参数',
|
||||
'description' => '允许用户访问服务器的启动参数和变量.',
|
||||
'title' => '查看启动参数',
|
||||
'description' => '允许用户访问服务器的启动参数和相关变量。',
|
||||
],
|
||||
'edit_startup' => [
|
||||
'title' => '编辑启动参数',
|
||||
'description' => '允许用户更改服务器的启动参数和变量.',
|
||||
'description' => '允许用户更改服务器的启动参数。',
|
||||
],
|
||||
'list_schedules' => [
|
||||
'title' => '列出计划任务',
|
||||
'description' => '允许用户列出服务器上的所有计划任务 (无论是否启用) .',
|
||||
'description' => '允许用户列出服务器上的所有计划任务(无论是否启用)。',
|
||||
],
|
||||
'view_schedule' => [
|
||||
'title' => '访问计划任务',
|
||||
'description' => '允许用户查看一个计划任务的具体信息,包括其执行的时间和命令.',
|
||||
'title' => '查看计划',
|
||||
'description' => '允许用户查看计划任务的详细信息,包含执行时间及分配任务。',
|
||||
],
|
||||
'toggle_schedule' => [
|
||||
'title' => '开关计划任务',
|
||||
'description' => '允许用户更改计划任务的启用或禁用状态.',
|
||||
'title' => '开关计划',
|
||||
'description' => '允许用户启用或禁用计划的。',
|
||||
],
|
||||
'queue_schedule' => [
|
||||
'title' => '队列化计划任务',
|
||||
'description' => '允许用户将一个计划任务队列,以便在下一个周期执行.',
|
||||
'title' => '队列计划',
|
||||
'description' => '允许用户将计划纳入队列在下个周期执行。',
|
||||
],
|
||||
'edit_schedule' => [
|
||||
'title' => '编辑计划任务',
|
||||
'description' => '允许用户编辑计划任务. 此权限允许用户删除所有的执行任务,但无法删除计划任务本身.',
|
||||
'title' => '编辑计划',
|
||||
'description' => '允许用户编辑计划,包括所有的执行任务。这将允许用户移除单个任务,但无法移除计划本身。',
|
||||
],
|
||||
'create_schedule' => [
|
||||
'title' => '创建计划任务',
|
||||
'description' => '允许用户创建一个计划任务.',
|
||||
'title' => '创建计划',
|
||||
'description' => '允许用户新建计划任务。',
|
||||
],
|
||||
'delete_schedule' => [
|
||||
'title' => '删除计划任务',
|
||||
'description' => '允许用户从服务器删除一个计划任务.',
|
||||
'title' => '删除计划',
|
||||
'description' => '允许用户从服务器删除计划。',
|
||||
],
|
||||
'view_databases' => [
|
||||
'title' => '访问数据库信息',
|
||||
'description' => '允许用户访问附属于此服务器的数据库信息,包含数据库的地址,用户名和密码',
|
||||
'title' => '查看数据库信息',
|
||||
'description' => '允许用户查看所有与此服务器相关联的数据库及其用户名与密码信息。',
|
||||
],
|
||||
'reset_db_password' => [
|
||||
'title' => '重设数据库',
|
||||
'description' => '允许用户重新设置服务器数据库的密码.',
|
||||
'title' => '重置数据库',
|
||||
'description' => '允许用户重置服务器数据库密码。',
|
||||
],
|
||||
'delete_database' => [
|
||||
'title' => '删除数据库',
|
||||
'description' => '允许用户从面板删除此服务器的数据库.',
|
||||
'description' => '允许用户从面板删除此服务器数据库。',
|
||||
],
|
||||
'create_database' => [
|
||||
'title' => '新建数据库',
|
||||
'description' => '允许用户为这个服务器创建一个数据库.',
|
||||
'description' => '允许用户为此服务器新建数据库。',
|
||||
],
|
||||
],
|
||||
],
|
||||
'allocations' => [
|
||||
'mass_actions' => '批量操作',
|
||||
'delete' => '删除分配地址',
|
||||
],
|
||||
'files' => [
|
||||
'exceptions' => [
|
||||
'invalid_mime' => '这种类型的文件无法使用面板内建编辑器编辑.',
|
||||
'max_size' => '此文件太大,无法使用面板内建编辑器编辑.',
|
||||
'invalid_mime' => '此类型文件无法通过面板内置编辑器编辑。',
|
||||
'max_size' => '此文件过大,无法使用面板内置编辑器编辑。',
|
||||
],
|
||||
'header' => '文件管理',
|
||||
'header_sub' => '从网页直接管理您所有的文件.',
|
||||
'loading' => '正在加载初始文件结构,这可能需要几秒钟.',
|
||||
'path' => '当你在配置任何插件或服务器设置的文件路径时 :path 应该为您的根目录. 此节点的网页上传最大文件限制为 :size.',
|
||||
'seconds_ago' => '几秒之前',
|
||||
'header_sub' => '从网页直接管理您的所有文件。',
|
||||
'loading' => '正在加载初始文件结构,这可能需要几秒钟。',
|
||||
'path' => '当您配置插件或服务器设置的文件路径时,您应使用 :path 作为您的根目录。此节点通过网页上传的最大文件限制为 :size。',
|
||||
'seconds_ago' => '数秒前',
|
||||
'file_name' => '文件名',
|
||||
'size' => '大小',
|
||||
'last_modified' => '最后修改',
|
||||
'add_new' => '新建文件',
|
||||
'add_folder' => '新建文件夹',
|
||||
'mass_actions' => '更多操作',
|
||||
'mass_actions' => '批量操作',
|
||||
'delete' => '删除文件',
|
||||
'edit' => [
|
||||
'header' => '编辑文件',
|
||||
'header_sub' => '从网页更改一个文件.',
|
||||
'header_sub' => '从网页编辑文件。',
|
||||
'save' => '保存文件',
|
||||
'return' => '返回文件管理',
|
||||
],
|
||||
'add' => [
|
||||
'header' => '新建文件',
|
||||
'header_sub' => '在您服务器上创建一个新文件.',
|
||||
'header_sub' => '在您服务器上新建新文件。',
|
||||
'name' => '文件名',
|
||||
'create' => '创建文件',
|
||||
],
|
||||
|
@ -291,40 +295,40 @@ return [
|
|||
'config' => [
|
||||
'name' => [
|
||||
'header' => '服务器名',
|
||||
'header_sub' => '更改您服务器的名称。',
|
||||
'details' => '此服务器名只是为了让你更好的管理服务器,并不会对服务器内的玩家有所影响.',
|
||||
'header_sub' => '更改服务器名称。',
|
||||
'details' => '此服务器名只是为了让您更好的管理服务器,并不会对向游戏内玩家显示的服务器配置造成影响。',
|
||||
],
|
||||
'startup' => [
|
||||
'header' => '启动配置',
|
||||
'header_sub' => '控制服务器的启动参数.',
|
||||
'header_sub' => '控制服务器的启动参数。',
|
||||
'command' => '启动命令',
|
||||
'edit_params' => '编辑参数',
|
||||
'update' => '更新启动参数',
|
||||
'startup_regex' => '输入规则',
|
||||
'edited' => '启动参数已成功更新. 更新的内容会在下一次启动时生效.',
|
||||
'edited' => '已成功编辑启动变量。这将在下次服务器启动时发挥功用。',
|
||||
],
|
||||
'sftp' => [
|
||||
'header' => 'SFTP 配置',
|
||||
'header_sub' => 'SFTP 连接所需要的信息.',
|
||||
'header_sub' => 'SFTP 连接所需的账户信息。',
|
||||
'details' => 'SFTP 信息',
|
||||
'conn_addr' => '连接地址',
|
||||
'warning' => 'SFTP密码就是您的用户密码. 请确认你使用的时SFTP,不是FTP,也不是FTPS, 这些都是不同的协议.',
|
||||
'warning' => 'SFTP 密码为您的账户密码。请确保您的客户端被设置为使用 SFTP 而非 FTP 或 FTPS,这些协议间存在差异。',
|
||||
],
|
||||
'database' => [
|
||||
'header' => '数据库',
|
||||
'header_sub' => '此服务器可用的数据库.',
|
||||
'header_sub' => '此服务器的所有可用数据库。',
|
||||
'your_dbs' => '已配置的数据库',
|
||||
'host' => 'MySQL 主机',
|
||||
'reset_password' => '重设密码',
|
||||
'no_dbs' => '没有此服务器可用的数据库.',
|
||||
'add_db' => '创建一个新数据库.',
|
||||
'reset_password' => '重置密码',
|
||||
'no_dbs' => '此服务器没有可用的数据库。',
|
||||
'add_db' => '新建新数据库。',
|
||||
],
|
||||
'allocation' => [
|
||||
'header' => '服务器连接信息',
|
||||
'header_sub' => '控制此服务器可用的IP和端口.',
|
||||
'available' => '可用的连接信息',
|
||||
'help' => '连接信息版主',
|
||||
'help_text' => '左边列出的所有IP和端口都是开放的,是您连接到您服务器的地址',
|
||||
'header' => '服务器地址分配',
|
||||
'header_sub' => '控制此服务器可使用的 IP 地址和端口。',
|
||||
'available' => '可用分配地址',
|
||||
'help' => '分配地址帮助',
|
||||
'help_text' => '左方列表列出了您可用于传入连接的所有可用 IP 地址及端口。',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'email' => 'Email',
|
||||
'user_identifier' => '用户名 或 Email',
|
||||
'email' => '电子邮件地址',
|
||||
'user_identifier' => '用户名或电子邮件地址',
|
||||
'password' => '密码',
|
||||
'confirm_password' => '确认密码',
|
||||
'login' => '登陆',
|
||||
'home' => '主页',
|
||||
'login' => '登录',
|
||||
'home' => '首页',
|
||||
'servers' => '服务器',
|
||||
'id' => 'ID',
|
||||
'id' => '编号',
|
||||
'name' => '名称',
|
||||
'node' => '节点',
|
||||
'connection' => '连接',
|
||||
|
@ -16,12 +16,12 @@ return [
|
|||
'cpu' => 'CPU',
|
||||
'status' => '状态',
|
||||
'search' => '搜索',
|
||||
'suspended' => '已暂停',
|
||||
'suspended' => '已停用',
|
||||
'account' => '用户',
|
||||
'security' => '安全',
|
||||
'ip' => 'IP 地址',
|
||||
'last_activity' => '上次活动',
|
||||
'revoke' => '吊销',
|
||||
'revoke' => '注销',
|
||||
'2fa_token' => '认证密钥',
|
||||
'submit' => '确认',
|
||||
'close' => '关闭',
|
||||
|
@ -29,11 +29,11 @@ return [
|
|||
'configuration' => '配置',
|
||||
'sftp' => 'SFTP',
|
||||
'databases' => '数据库',
|
||||
'memo' => 'Memo',
|
||||
'memo' => '描述',
|
||||
'created' => '已创建',
|
||||
'expires' => '过期',
|
||||
'public_key' => '令牌',
|
||||
'api_access' => 'Api 访问',
|
||||
'api_access' => 'API 访问',
|
||||
'never' => '从未',
|
||||
'sign_out' => '登出',
|
||||
'admin_control' => '管理员面板',
|
||||
|
@ -64,25 +64,25 @@ return [
|
|||
'2fa' => '两步验证',
|
||||
'logout' => '登出',
|
||||
'admin_cp' => '管理员控制面板',
|
||||
'optional' => '可选的',
|
||||
'optional' => '可选项',
|
||||
'read_only' => '只读',
|
||||
'relation' => '关系',
|
||||
'owner' => '所有者',
|
||||
'admin' => '管理员',
|
||||
'subuser' => '子用户',
|
||||
'captcha_invalid' => '输入的验证码错误.',
|
||||
'captcha_invalid' => '验证码无效',
|
||||
'tasks' => '任务',
|
||||
'seconds' => '秒',
|
||||
'minutes' => '分',
|
||||
'under_maintenance' => '维护中',
|
||||
'days' => [
|
||||
'sun' => '周日',
|
||||
'mon' => '周一',
|
||||
'tues' => '周二',
|
||||
'wed' => '周三',
|
||||
'thurs' => '周四',
|
||||
'fri' => '周五',
|
||||
'sat' => '周六',
|
||||
'sun' => '星期天',
|
||||
'mon' => '星期一',
|
||||
'tues' => '星期二',
|
||||
'wed' => '星期三',
|
||||
'thurs' => '星期四',
|
||||
'fri' => '星期五',
|
||||
'sat' => '星期六',
|
||||
],
|
||||
'last_used' => '上次使用',
|
||||
];
|
||||
|
|
|
@ -12,78 +12,78 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'accepted' => ' :attribute 被接受.',
|
||||
'active_url' => ' :attribute 不是一个有效的URL.',
|
||||
'after' => ' :attribute 必须是一个位于 :date 之后的日期.',
|
||||
'after_or_equal' => ' :attribute 必须是 :date 之后或同样的日期.',
|
||||
'alpha' => ' :attribute 只能含有字母.',
|
||||
'alpha_dash' => ':attribute 只能含有数字字母和分隔线.',
|
||||
'alpha_num' => ' :attribute 只能含有数字和字母.',
|
||||
'array' => ' :attribute 必须是个数组.',
|
||||
'before' => ' :attribute 必须是一个位于 :date 之前的日前.',
|
||||
'before_or_equal' => ' :attribute 必须是 :date 之前或同样的日期.',
|
||||
'accepted' => 'The :attribute must be accepted.',
|
||||
'active_url' => 'The :attribute is not a valid URL.',
|
||||
'after' => 'The :attribute must be a date after :date.',
|
||||
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
|
||||
'alpha' => 'The :attribute may only contain letters.',
|
||||
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
|
||||
'alpha_num' => 'The :attribute may only contain letters and numbers.',
|
||||
'array' => 'The :attribute must be an array.',
|
||||
'before' => 'The :attribute must be a date before :date.',
|
||||
'before_or_equal' => 'The :attribute must be a date before or equal to :date.',
|
||||
'between' => [
|
||||
'numeric' => ' :attribute 必须在 :min 到 :max 之间.',
|
||||
'file' => ' :attribute 必须在 :min 到 :max KB 之间.',
|
||||
'string' => ' :attribute m必须在 :min 到 :max 个字符之间.',
|
||||
'array' => ' :attribute 必须在 :min 到 :max 个项目之间.',
|
||||
'numeric' => 'The :attribute must be between :min and :max.',
|
||||
'file' => 'The :attribute must be between :min and :max kilobytes.',
|
||||
'string' => 'The :attribute must be between :min and :max characters.',
|
||||
'array' => 'The :attribute must have between :min and :max items.',
|
||||
],
|
||||
'boolean' => ' :attribute 填入的必须为 true 或 false.',
|
||||
'confirmed' => ' :attribute 确认不匹配.',
|
||||
'date' => ' :attribute 不是一个合法的日期.',
|
||||
'date_format' => ' :attribute 不是正确的格式: :format.',
|
||||
'different' => ' :attribute 和 :other 必须不同.',
|
||||
'digits' => ' :attribute 必须为 :digits 个数字.',
|
||||
'digits_between' => ' :attribute 必须在 :min 到 :max 个数字间.',
|
||||
'dimensions' => ' :attribute 有一个非法的镜像大小.',
|
||||
'distinct' => ' :attribute 填入了一个重复的值.',
|
||||
'email' => ' :attribute 必须是一个合法的Email地址.',
|
||||
'exists' => '所选择的 :attribute 无效.',
|
||||
'file' => ' :attribute 必须为一个文件.',
|
||||
'filled' => ' :attribute 为必填项目.',
|
||||
'image' => ' :attribute 必须是一个镜像.',
|
||||
'in' => '所选择的 :attribute 无效.',
|
||||
'in_array' => ' :attribute 填入的信息在 :other 不存在.',
|
||||
'integer' => ' :attribute 必须是一个整数.',
|
||||
'ip' => ' :attribute 必须是一个合法的IP地址.',
|
||||
'json' => ' :attribute 必须是一个合法的JSON字符串.',
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'confirmed' => 'The :attribute confirmation does not match.',
|
||||
'date' => 'The :attribute is not a valid date.',
|
||||
'date_format' => 'The :attribute does not match the format :format.',
|
||||
'different' => 'The :attribute and :other must be different.',
|
||||
'digits' => 'The :attribute must be :digits digits.',
|
||||
'digits_between' => 'The :attribute must be between :min and :max digits.',
|
||||
'dimensions' => 'The :attribute has invalid image dimensions.',
|
||||
'distinct' => 'The :attribute field has a duplicate value.',
|
||||
'email' => 'The :attribute must be a valid email address.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'file' => 'The :attribute must be a file.',
|
||||
'filled' => 'The :attribute field is required.',
|
||||
'image' => 'The :attribute must be an image.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field does not exist in :other.',
|
||||
'integer' => 'The :attribute must be an integer.',
|
||||
'ip' => 'The :attribute must be a valid IP address.',
|
||||
'json' => 'The :attribute must be a valid JSON string.',
|
||||
'max' => [
|
||||
'numeric' => ' :attribute 不能大于 :max.',
|
||||
'file' => ' :attribute 不能大于 :max KB.',
|
||||
'string' => ' :attribute 不能多于 :max 个字符.',
|
||||
'array' => ' :attribute 不能多于 :max 个项目.',
|
||||
'numeric' => 'The :attribute may not be greater than :max.',
|
||||
'file' => 'The :attribute may not be greater than :max kilobytes.',
|
||||
'string' => 'The :attribute may not be greater than :max characters.',
|
||||
'array' => 'The :attribute may not have more than :max items.',
|
||||
],
|
||||
'mimes' => ' :attribute 文件类型必须为: :values.',
|
||||
'mimetypes' => ' :attribute 文件类型必须为: :values.',
|
||||
'mimes' => 'The :attribute must be a file of type: :values.',
|
||||
'mimetypes' => 'The :attribute must be a file of type: :values.',
|
||||
'min' => [
|
||||
'numeric' => ' :attribute 至少应在 :min.',
|
||||
'file' => ' :attribute 至少应在 :min KB.',
|
||||
'string' => ' :attribute 至少应在 :min 个字符.',
|
||||
'array' => ' :attribute 至少应有 :min 个项目.',
|
||||
'numeric' => 'The :attribute must be at least :min.',
|
||||
'file' => 'The :attribute must be at least :min kilobytes.',
|
||||
'string' => 'The :attribute must be at least :min characters.',
|
||||
'array' => 'The :attribute must have at least :min items.',
|
||||
],
|
||||
'not_in' => '所选择的 :attribute 不正确.',
|
||||
'numeric' => ' :attribute 必须是个数字.',
|
||||
'present' => ' :attribute 填入的必须存在.',
|
||||
'regex' => ' :attribute 格式不正确.',
|
||||
'required' => ' :attribute 为必填.',
|
||||
'required_if' => ' :attribute 被要求填入, 当 :other 为 :value 的时候.',
|
||||
'required_unless' => ' :attribute 被要求填入,除非 :other 为 :values.',
|
||||
'required_with' => ' :attribute 被要求填入,当 :values 存在的时候.',
|
||||
'required_with_all' => ' :attribute 被要求填入,当 :values 存在.',
|
||||
'required_without' => ' :attribute 被要求填入,当 :values 不存在.',
|
||||
'required_without_all' => ' :attribute 被要求填入,当 :values 都不存在.',
|
||||
'same' => ' :attribute 和 :other 必须相同.',
|
||||
'not_in' => 'The selected :attribute is invalid.',
|
||||
'numeric' => 'The :attribute must be a number.',
|
||||
'present' => 'The :attribute field must be present.',
|
||||
'regex' => 'The :attribute format is invalid.',
|
||||
'required' => 'The :attribute field is required.',
|
||||
'required_if' => 'The :attribute field is required when :other is :value.',
|
||||
'required_unless' => 'The :attribute field is required unless :other is in :values.',
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_with_all' => 'The :attribute field is required when :values is present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'required_without_all' => 'The :attribute field is required when none of :values are present.',
|
||||
'same' => 'The :attribute and :other must match.',
|
||||
'size' => [
|
||||
'numeric' => ' :attribute 必须为 :size.',
|
||||
'file' => ' :attribute 必须为 :size KB.',
|
||||
'string' => ' :attribute 必须为 :size 个字符.',
|
||||
'array' => ' :attribute 必须包含 :size 个项目.',
|
||||
'numeric' => 'The :attribute must be :size.',
|
||||
'file' => 'The :attribute must be :size kilobytes.',
|
||||
'string' => 'The :attribute must be :size characters.',
|
||||
'array' => 'The :attribute must contain :size items.',
|
||||
],
|
||||
'string' => ' :attribute 必须为字符串.',
|
||||
'timezone' => ' :attribute 必须是一个有效的时区.',
|
||||
'unique' => ' :attribute 已经被使用.',
|
||||
'uploaded' => ' :attribute 上传失败.',
|
||||
'url' => ' :attribute 格式不合法.',
|
||||
'string' => 'The :attribute must be a string.',
|
||||
'timezone' => 'The :attribute must be a valid zone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
'url' => 'The :attribute format is invalid.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
36
resources/scripts/.eslintrc.yml
Normal file
36
resources/scripts/.eslintrc.yml
Normal file
|
@ -0,0 +1,36 @@
|
|||
parser: "@typescript-eslint/parser"
|
||||
parserOptions:
|
||||
ecmaVersion: 6
|
||||
project: "./tsconfig.json"
|
||||
tsconfigRootDir: "./"
|
||||
env:
|
||||
browser: true
|
||||
es6: true
|
||||
plugins:
|
||||
- "@typescript-eslint"
|
||||
extends:
|
||||
- "standard"
|
||||
- "plugin:@typescript-eslint/recommended"
|
||||
rules:
|
||||
semi:
|
||||
- error
|
||||
- always
|
||||
comma-dangle:
|
||||
- error
|
||||
- always-multiline
|
||||
"@typescript-eslint/explicit-function-return-type": 0
|
||||
"@typescript-eslint/explicit-member-accessibility": 0
|
||||
"@typescript-eslint/no-unused-vars": 0
|
||||
"@typescript-eslint/no-explicit-any": 0
|
||||
"@typescript-eslint/no-non-null-assertion": 0
|
||||
overrides:
|
||||
- files:
|
||||
- "**/*.tsx"
|
||||
rules:
|
||||
operator-linebreak:
|
||||
- error
|
||||
- before
|
||||
- overrides:
|
||||
"&&": "after"
|
||||
"?": "ignore"
|
||||
":": "ignore"
|
32
resources/scripts/TransitionRouter.tsx
Normal file
32
resources/scripts/TransitionRouter.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { Route } from 'react-router';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
|
||||
type Props = Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>;
|
||||
|
||||
export default ({ children }: Props) => (
|
||||
<Route
|
||||
render={({ location }) => (
|
||||
<TransitionGroup className={'route-transition-group'}>
|
||||
<CSSTransition key={location.key} timeout={250} classNames={'fade'}>
|
||||
<section>
|
||||
{children}
|
||||
<div className={'mx-auto w-full'} style={{ maxWidth: '1200px' }}>
|
||||
<p className={'text-right text-neutral-500 text-xs'}>
|
||||
© 2015 - 2019
|
||||
<a
|
||||
href={'https://pterodactyl.io'}
|
||||
className={'no-underline text-neutral-500 hover:text-neutral-300'}
|
||||
>
|
||||
Pterodactyl Software
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</CSSTransition>
|
||||
</TransitionGroup>
|
||||
)}
|
||||
/>
|
||||
);
|
9
resources/scripts/api/account/updateAccountEmail.ts
Normal file
9
resources/scripts/api/account/updateAccountEmail.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default (email: string, password: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.put('/api/client/account/email', { email, password })
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
21
resources/scripts/api/account/updateAccountPassword.ts
Normal file
21
resources/scripts/api/account/updateAccountPassword.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
interface Data {
|
||||
current: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export default ({ current, password, confirmPassword }: Data): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.put('/api/client/account/password', {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
current_password: current,
|
||||
password: password,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
password_confirmation: confirmPassword,
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
25
resources/scripts/api/auth/login.ts
Normal file
25
resources/scripts/api/auth/login.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export interface LoginResponse {
|
||||
complete: boolean;
|
||||
intended?: string;
|
||||
confirmationToken?: string;
|
||||
}
|
||||
|
||||
export default (user: string, password: string): Promise<LoginResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post('/auth/login', { user, password })
|
||||
.then(response => {
|
||||
if (!(response.data instanceof Object)) {
|
||||
return reject(new Error('An error occurred while processing the login request.'));
|
||||
}
|
||||
|
||||
return resolve({
|
||||
complete: response.data.data.complete,
|
||||
intended: response.data.data.intended || undefined,
|
||||
confirmationToken: response.data.data.confirmation_token || undefined,
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
18
resources/scripts/api/auth/loginCheckpoint.ts
Normal file
18
resources/scripts/api/auth/loginCheckpoint.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import http from '@/api/http';
|
||||
import { LoginResponse } from '@/api/auth/login';
|
||||
|
||||
export default (token: string, code: string): Promise<LoginResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post('/auth/login/checkpoint', {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
confirmation_token: token,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
authentication_code: code,
|
||||
})
|
||||
.then(response => resolve({
|
||||
complete: response.data.data.complete,
|
||||
intended: response.data.data.intended || undefined,
|
||||
}))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
29
resources/scripts/api/auth/performPasswordReset.ts
Normal file
29
resources/scripts/api/auth/performPasswordReset.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
interface Data {
|
||||
token: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
}
|
||||
|
||||
interface PasswordResetResponse {
|
||||
redirectTo?: string | null;
|
||||
sendToLogin: boolean;
|
||||
}
|
||||
|
||||
export default (email: string, data: Data): Promise<PasswordResetResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post('/auth/password/reset', {
|
||||
email,
|
||||
token: data.token,
|
||||
password: data.password,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
password_confirmation: data.passwordConfirmation,
|
||||
})
|
||||
.then(response => resolve({
|
||||
redirectTo: response.data.redirect_to,
|
||||
sendToLogin: response.data.send_to_login,
|
||||
}))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
9
resources/scripts/api/auth/requestPasswordResetEmail.ts
Normal file
9
resources/scripts/api/auth/requestPasswordResetEmail.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default (email: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post('/auth/password', { email })
|
||||
.then(response => resolve(response.data.status || ''))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
43
resources/scripts/api/http.ts
Normal file
43
resources/scripts/api/http.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
// This token is set in the bootstrap.js file at the beginning of the request
|
||||
// and is carried through from there.
|
||||
// const token: string = '';
|
||||
|
||||
const http: AxiosInstance = axios.create({
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': (window as any).X_CSRF_TOKEN as string || '',
|
||||
},
|
||||
});
|
||||
|
||||
// If we have a phpdebugbar instance registered at this point in time go
|
||||
// ahead and route the response data through to it so things show up.
|
||||
// @ts-ignore
|
||||
if (typeof window.phpdebugbar !== 'undefined') {
|
||||
http.interceptors.response.use(response => {
|
||||
// @ts-ignore
|
||||
window.phpdebugbar.ajaxHandler.handle(response.request);
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
export default http;
|
||||
|
||||
/**
|
||||
* Converts an error into a human readable response. Mostly just a generic helper to
|
||||
* make sure we display the message from the server back to the user if we can.
|
||||
*/
|
||||
export function httpErrorToHuman (error: any): string {
|
||||
if (error.response && error.response.data) {
|
||||
const { data } = error.response;
|
||||
if (data.errors && data.errors[0] && data.errors[0].detail) {
|
||||
return data.errors[0].detail;
|
||||
}
|
||||
}
|
||||
|
||||
return error.message;
|
||||
}
|
15
resources/scripts/api/server/createServerDatabase.ts
Normal file
15
resources/scripts/api/server/createServerDatabase.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/getServerDatabases';
|
||||
import http from '@/api/http';
|
||||
|
||||
export default (uuid: string, data: { connectionsFrom: string; databaseName: string }): Promise<ServerDatabase> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(`/api/client/servers/${uuid}/databases`, {
|
||||
database: data.databaseName,
|
||||
remote: data.connectionsFrom,
|
||||
}, {
|
||||
params: { include: 'password' },
|
||||
})
|
||||
.then(response => resolve(rawDataToServerDatabase(response.data.attributes)))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
9
resources/scripts/api/server/deleteServerDatabase.ts
Normal file
9
resources/scripts/api/server/deleteServerDatabase.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default (uuid: string, database: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.delete(`/api/client/servers/${uuid}/databases/${database}`)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
50
resources/scripts/api/server/getServer.ts
Normal file
50
resources/scripts/api/server/getServer.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export interface Allocation {
|
||||
ip: string;
|
||||
alias: string | null;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
id: string;
|
||||
uuid: string;
|
||||
name: string;
|
||||
node: string;
|
||||
description: string;
|
||||
allocations: Allocation[];
|
||||
limits: {
|
||||
memory: number;
|
||||
swap: number;
|
||||
disk: number;
|
||||
io: number;
|
||||
cpu: number;
|
||||
};
|
||||
featureLimits: {
|
||||
databases: number;
|
||||
allocations: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const rawDataToServerObject = (data: any): Server => ({
|
||||
id: data.identifier,
|
||||
uuid: data.uuid,
|
||||
name: data.name,
|
||||
node: data.node,
|
||||
description: data.description ? ((data.description.length > 0) ? data.description : null) : null,
|
||||
allocations: [{
|
||||
ip: data.allocation.ip,
|
||||
alias: null,
|
||||
port: data.allocation.port,
|
||||
}],
|
||||
limits: { ...data.limits },
|
||||
featureLimits: { ...data.feature_limits },
|
||||
});
|
||||
|
||||
export default (uuid: string): Promise<Server> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}`)
|
||||
.then(response => resolve(rawDataToServerObject(response.data.attributes)))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
31
resources/scripts/api/server/getServerDatabases.ts
Normal file
31
resources/scripts/api/server/getServerDatabases.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export interface ServerDatabase {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
connectionString: string;
|
||||
allowConnectionsFrom: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export const rawDataToServerDatabase = (data: any): ServerDatabase => ({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
username: data.username,
|
||||
connectionString: `${data.host.address}:${data.host.port}`,
|
||||
allowConnectionsFrom: data.connections_from,
|
||||
password: data.relationships && data.relationships.password ? data.relationships.password.attributes.password : undefined,
|
||||
});
|
||||
|
||||
export default (uuid: string, includePassword: boolean = true): Promise<ServerDatabase[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}/databases`, {
|
||||
params: includePassword ? { include: 'password' } : undefined,
|
||||
})
|
||||
.then(response => resolve(
|
||||
(response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes))
|
||||
))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
58
resources/scripts/components/App.tsx
Normal file
58
resources/scripts/components/App.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import * as React from 'react';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import { BrowserRouter, BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { StoreProvider } from 'easy-peasy';
|
||||
import { store } from '@/state';
|
||||
import DashboardRouter from '@/routers/DashboardRouter';
|
||||
import ServerRouter from '@/routers/ServerRouter';
|
||||
import AuthenticationRouter from '@/routers/AuthenticationRouter';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
interface WindowWithUser extends Window {
|
||||
PterodactylUser?: {
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
root_admin: boolean;
|
||||
use_totp: boolean;
|
||||
language: string;
|
||||
updated_at: string;
|
||||
created_at: string;
|
||||
};
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const data = (window as WindowWithUser).PterodactylUser;
|
||||
if (data && !store.getState().user.data) {
|
||||
store.getActions().user.setUserData({
|
||||
uuid: data.uuid,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
language: data.language,
|
||||
rootAdmin: data.root_admin,
|
||||
useTotp: data.use_totp,
|
||||
createdAt: new Date(data.created_at),
|
||||
updatedAt: new Date(data.updated_at),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<StoreProvider store={store}>
|
||||
<Provider store={store}>
|
||||
<Router basename={'/'}>
|
||||
<div className={'mx-auto w-auto'}>
|
||||
<BrowserRouter basename={'/'}>
|
||||
<Switch>
|
||||
<Route path="/server/:id" component={ServerRouter}/>
|
||||
<Route path="/auth" component={AuthenticationRouter}/>
|
||||
<Route path="/" component={DashboardRouter}/>
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</StoreProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default hot(App);
|
38
resources/scripts/components/FlashMessageRender.tsx
Normal file
38
resources/scripts/components/FlashMessageRender.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import MessageBox from '@/components/MessageBox';
|
||||
import { State, useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
type Props = Readonly<{
|
||||
byKey?: string;
|
||||
spacerClass?: string;
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export default ({ className, spacerClass, byKey }: Props) => {
|
||||
const flashes = useStoreState((state: State<ApplicationStore>) => state.flashes.items);
|
||||
|
||||
let filtered = flashes;
|
||||
if (byKey) {
|
||||
filtered = flashes.filter(flash => flash.key === byKey);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{
|
||||
filtered.map((flash, index) => (
|
||||
<React.Fragment key={flash.id || flash.type + flash.message}>
|
||||
{index > 0 && <div className={spacerClass || 'mt-2'}></div>}
|
||||
<MessageBox type={flash.type} title={flash.title}>
|
||||
{flash.message}
|
||||
</MessageBox>
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
18
resources/scripts/components/MessageBox.tsx
Normal file
18
resources/scripts/components/MessageBox.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
children: string;
|
||||
type?: FlashMessageType;
|
||||
}
|
||||
|
||||
export default ({ title, children, type }: Props) => (
|
||||
<div className={`lg:inline-flex alert ${type}`} role={'alert'}>
|
||||
{title && <span className={'title'}>{title}</span>}
|
||||
<span className={'message'}>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
35
resources/scripts/components/NavigationBar.tsx
Normal file
35
resources/scripts/components/NavigationBar.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import * as React from 'react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLayerGroup } from '@fortawesome/free-solid-svg-icons/faLayerGroup';
|
||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons/faUserCircle';
|
||||
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt';
|
||||
import { faSwatchbook } from '@fortawesome/free-solid-svg-icons/faSwatchbook';
|
||||
|
||||
export default () => (
|
||||
<div id={'navigation'}>
|
||||
<div className={'mx-auto w-full flex items-center'} style={{ maxWidth: '1200px', height: '3.5rem' }}>
|
||||
<div id={'logo'}>
|
||||
<Link to={'/'}>
|
||||
Pterodactyl
|
||||
</Link>
|
||||
</div>
|
||||
<div className={'right-navigation'}>
|
||||
<NavLink to={'/'} exact={true}>
|
||||
<FontAwesomeIcon icon={faLayerGroup}/>
|
||||
</NavLink>
|
||||
<NavLink to={'/account'}>
|
||||
<FontAwesomeIcon icon={faUserCircle}/>
|
||||
</NavLink>
|
||||
{process.env.NODE_ENV !== 'production' &&
|
||||
<NavLink to={'/design'}>
|
||||
<FontAwesomeIcon icon={faSwatchbook}/>
|
||||
</NavLink>
|
||||
}
|
||||
<NavLink to={'/auth/logout'}>
|
||||
<FontAwesomeIcon icon={faSignOutAlt}/>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
13
resources/scripts/components/NetworkErrorMessage.tsx
Normal file
13
resources/scripts/components/NetworkErrorMessage.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as React from 'react';
|
||||
import MessageBox from '@/components/MessageBox';
|
||||
|
||||
export default ({ message }: { message: string | undefined | null }) => (
|
||||
!message ?
|
||||
null
|
||||
:
|
||||
<div className={'mb-4'}>
|
||||
<MessageBox type={'error'} title={'Error'}>
|
||||
{message}
|
||||
</MessageBox>
|
||||
</div>
|
||||
);
|
13
resources/scripts/components/ServerOverviewContainer.tsx
Normal file
13
resources/scripts/components/ServerOverviewContainer.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
export default class ServerOverviewContainer extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<div className={'mt-10'}>
|
||||
<NavLink className={'text-neutral-100 text-sm block mb-2 no-underline hover:underline'} to={'/account'}>Account</NavLink>
|
||||
<NavLink className={'text-neutral-100 text-sm block mb-2 no-underline hover:underline'} to={'/account/design'}>Design</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
export default () => {
|
||||
const [ isSubmitting, setSubmitting ] = React.useState(false);
|
||||
const [ email, setEmail ] = React.useState('');
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const handleFieldUpdate = (e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value);
|
||||
|
||||
const handleSubmission = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setSubmitting(true);
|
||||
clearFlashes();
|
||||
requestPasswordResetEmail(email)
|
||||
.then(response => {
|
||||
setEmail('');
|
||||
addFlash({ type: 'success', title: 'Success', message: response });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
Request Password Reset
|
||||
</h2>
|
||||
<FlashMessageRender/>
|
||||
<LoginFormContainer onSubmit={handleSubmission}>
|
||||
<label htmlFor={'email'}>Email</label>
|
||||
<input
|
||||
id={'email'}
|
||||
type={'email'}
|
||||
required={true}
|
||||
className={'input'}
|
||||
value={email}
|
||||
onChange={handleFieldUpdate}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<p className={'input-help'}>
|
||||
Enter your account email address to receive instructions on resetting your password.
|
||||
</p>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
className={'btn btn-primary btn-jumbo flex justify-center'}
|
||||
disabled={isSubmitting || email.length < 5}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<div className={'spinner-circle spinner-sm spinner-white'}></div>
|
||||
:
|
||||
'Send Email'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import loginCheckpoint from '@/api/auth/loginCheckpoint';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { StaticContext } from 'react-router';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
export default ({ history, location: { state } }: RouteComponentProps<{}, StaticContext, { token?: string }>) => {
|
||||
const [ code, setCode ] = useState('');
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
if (!state || !state.token) {
|
||||
history.replace('/auth/login');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value.length <= 6) {
|
||||
setCode(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsLoading(true);
|
||||
clearFlashes();
|
||||
|
||||
loginCheckpoint(state.token!, code)
|
||||
.then(response => {
|
||||
if (response.complete) {
|
||||
// @ts-ignore
|
||||
window.location = response.intended || '/';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
Device Checkpoint
|
||||
</h2>
|
||||
<FlashMessageRender/>
|
||||
<LoginFormContainer onSubmit={submit}>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'authentication_code'}>Authentication Code</label>
|
||||
<input
|
||||
id={'authentication_code'}
|
||||
type={'number'}
|
||||
autoFocus={true}
|
||||
className={'input'}
|
||||
value={code}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
disabled={isLoading || code.length !== 6}
|
||||
>
|
||||
{isLoading ?
|
||||
<span className={'spinner white'}> </span>
|
||||
:
|
||||
'Continue'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
94
resources/scripts/components/auth/LoginContainer.tsx
Normal file
94
resources/scripts/components/auth/LoginContainer.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import login from '@/api/auth/login';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
export default ({ history }: RouteComponentProps) => {
|
||||
const [ username, setUsername ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [ isLoading, setLoading ] = useState(false);
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
clearFlashes();
|
||||
|
||||
login(username!, password!)
|
||||
.then(response => {
|
||||
if (response.complete) {
|
||||
// @ts-ignore
|
||||
window.location = response.intended || '/';
|
||||
return;
|
||||
}
|
||||
|
||||
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setLoading(false);
|
||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||
});
|
||||
};
|
||||
|
||||
const canSubmit = () => username && password && username.length > 0 && password.length > 0;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
Login to Continue
|
||||
</h2>
|
||||
<FlashMessageRender/>
|
||||
<LoginFormContainer onSubmit={submit}>
|
||||
<label htmlFor={'username'}>Username or Email</label>
|
||||
<input
|
||||
id={'username'}
|
||||
autoFocus={true}
|
||||
required={true}
|
||||
className={'input'}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'password'}>Password</label>
|
||||
<input
|
||||
id={'password'}
|
||||
required={true}
|
||||
type={'password'}
|
||||
className={'input'}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
disabled={isLoading || !canSubmit()}
|
||||
>
|
||||
{isLoading ?
|
||||
<span className={'spinner white'}> </span>
|
||||
:
|
||||
'Login'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/password'}
|
||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
18
resources/scripts/components/auth/LoginFormContainer.tsx
Normal file
18
resources/scripts/components/auth/LoginFormContainer.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export default ({ className, ...props }: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>) => (
|
||||
<form
|
||||
className={'flex items-center justify-center login-box'}
|
||||
{...props}
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
>
|
||||
<div className={'flex-none select-none'}>
|
||||
<img src={'/assets/pterodactyl.svg'} className={'w-64'}/>
|
||||
</div>
|
||||
<div className={'flex-1'}>
|
||||
{props.children}
|
||||
</div>
|
||||
</form>
|
||||
);
|
109
resources/scripts/components/auth/ResetPasswordContainer.tsx
Normal file
109
resources/scripts/components/auth/ResetPasswordContainer.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import React, { useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { parse } from 'query-string';
|
||||
import { Link } from 'react-router-dom';
|
||||
import performPasswordReset from '@/api/auth/performPasswordReset';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
type Props = Readonly<RouteComponentProps<{ token: string }> & {}>;
|
||||
|
||||
export default (props: Props) => {
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
const [ email, setEmail ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [ passwordConfirm, setPasswordConfirm ] = useState('');
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const parsed = parse(props.location.search);
|
||||
if (email.length === 0 && parsed.email) {
|
||||
setEmail(parsed.email as string);
|
||||
}
|
||||
|
||||
const canSubmit = () => password && email && password.length >= 8 && password === passwordConfirm;
|
||||
|
||||
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!password || !email || !passwordConfirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
clearFlashes();
|
||||
|
||||
performPasswordReset(email, {
|
||||
token: props.match.params.token, password, passwordConfirmation: passwordConfirm,
|
||||
})
|
||||
.then(() => {
|
||||
addFlash({ type: 'success', message: 'Your password has been reset, please login to continue.' });
|
||||
props.history.push('/auth/login');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||
})
|
||||
.then(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
Reset Password
|
||||
</h2>
|
||||
<FlashMessageRender/>
|
||||
<LoginFormContainer onSubmit={submit}>
|
||||
<label>Email</label>
|
||||
<input className={'input'} value={email} disabled={true}/>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'new_password'}>New Password</label>
|
||||
<input
|
||||
id={'new_password'}
|
||||
className={'input'}
|
||||
type={'password'}
|
||||
required={true}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<p className={'input-help'}>
|
||||
Passwords must be at least 8 characters in length.
|
||||
</p>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'new_password_confirm'}>Confirm New Password</label>
|
||||
<input
|
||||
id={'new_password_confirm'}
|
||||
className={'input'}
|
||||
type={'password'}
|
||||
required={true}
|
||||
onChange={e => setPasswordConfirm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
disabled={isLoading || !canSubmit()}
|
||||
>
|
||||
{isLoading ?
|
||||
<span className={'spinner white'}> </span>
|
||||
:
|
||||
'Reset Password'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
import * as React from 'react';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
||||
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<div className={'flex my-10'}>
|
||||
<ContentBox className={'flex-1 mr-4'} title={'Update Password'} showFlashes={'account:password'}>
|
||||
<UpdatePasswordForm/>
|
||||
</ContentBox>
|
||||
<ContentBox className={'flex-1 ml-4'} title={'Update Email Address'} showFlashes={'account:email'}>
|
||||
<UpdateEmailAddressForm/>
|
||||
</ContentBox>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
|
||||
import { faMicrochip } from '@fortawesome/free-solid-svg-icons/faMicrochip';
|
||||
import { faMemory } from '@fortawesome/free-solid-svg-icons/faMemory';
|
||||
import { faHdd } from '@fortawesome/free-solid-svg-icons/faHdd';
|
||||
import { faEthernet } from '@fortawesome/free-solid-svg-icons/faEthernet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default () => (
|
||||
<div className={'my-10'}>
|
||||
<Link to={'/server/e9d6c836'} className={'grey-row-box cursor-pointer'}>
|
||||
<div className={'icon'}>
|
||||
<FontAwesomeIcon icon={faServer}/>
|
||||
</div>
|
||||
<div className={'w-1/2 ml-4'}>
|
||||
<p className={'text-lg'}>Party Parrots</p>
|
||||
</div>
|
||||
<div className={'flex flex-1 items-baseline justify-around'}>
|
||||
<div className={'flex ml-4'}>
|
||||
<FontAwesomeIcon icon={faEthernet} className={'text-neutral-500'}/>
|
||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
||||
192.168.100.100:25565
|
||||
</p>
|
||||
</div>
|
||||
<div className={'flex ml-4'}>
|
||||
<FontAwesomeIcon icon={faMicrochip} className={'text-neutral-500'}/>
|
||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
||||
34.6%
|
||||
</p>
|
||||
</div>
|
||||
<div className={'ml-4'}>
|
||||
<div className={'flex'}>
|
||||
<FontAwesomeIcon icon={faMemory} className={'text-neutral-500'}/>
|
||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
||||
2094 MB
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-xs text-neutral-600 text-center mt-1'}>of 4096 MB</p>
|
||||
</div>
|
||||
<div className={'ml-4'}>
|
||||
<div className={'flex'}>
|
||||
<FontAwesomeIcon icon={faHdd} className={'text-neutral-500'}/>
|
||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
||||
278 MB
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-xs text-neutral-600 text-center mt-1'}>of 16 GB</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className={'grey-row-box cursor-pointer mt-2'}>
|
||||
<div className={'icon'}>
|
||||
<FontAwesomeIcon icon={faServer}/>
|
||||
</div>
|
||||
<div className={'w-1/2 ml-4'}>
|
||||
<p className={'text-lg'}>My Factions Server</p>
|
||||
<p className={'text-neutral-400 text-xs mt-1'}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
|
||||
et dolore magna aliqua.
|
||||
</p>
|
||||
</div>
|
||||
<div className={'flex flex-1 items-baseline justify-around'}>
|
||||
<div className={'flex ml-4'}>
|
||||
<FontAwesomeIcon icon={faEthernet} className={'text-neutral-500'}/>
|
||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
||||
192.168.202.10:34556
|
||||
</p>
|
||||
</div>
|
||||
<div className={'flex ml-4'}>
|
||||
<FontAwesomeIcon icon={faMicrochip} className={'text-red-400'}/>
|
||||
<p className={'text-sm text-white ml-2'}>
|
||||
98.2 %
|
||||
</p>
|
||||
</div>
|
||||
<div className={'ml-4'}>
|
||||
<div className={'flex'}>
|
||||
<FontAwesomeIcon icon={faMemory} className={'text-neutral-500'}/>
|
||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
||||
376 MB
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-xs text-neutral-600 text-center mt-1'}>of 1024 MB</p>
|
||||
</div>
|
||||
<div className={'ml-4'}>
|
||||
<div className={'flex'}>
|
||||
<FontAwesomeIcon icon={faHdd} className={'text-neutral-500'}/>
|
||||
<p className={'text-sm text-neutral-400 ml-2'}>
|
||||
187 MB
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-xs text-neutral-600 text-center mt-1'}>of 32 GB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,75 @@
|
|||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
|
||||
export default class DesignElementsContainer extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={'my-10'}>
|
||||
<div className={'flex'}>
|
||||
<ContentBox
|
||||
className={'flex-1 mr-4'}
|
||||
title={'A Special Announcement'}
|
||||
borderColor={'border-primary-400'}
|
||||
>
|
||||
<p className={'text-neutral-200 text-sm'}>
|
||||
Your demands have been received: Dark Mode will be default in Pterodactyl 0.8!
|
||||
</p>
|
||||
<p><Link to={'/'}>Back</Link></p>
|
||||
</ContentBox>
|
||||
<div className={'ml-4 flex-1'}>
|
||||
<h2 className={'text-neutral-300 mb-2 px-4'}>Form Elements</h2>
|
||||
<div className={'bg-neutral-700 p-4 rounded shadow-lg border-t-4 border-primary-400'}>
|
||||
<label className={'uppercase text-neutral-200'}>Email</label>
|
||||
<input type={'text'} className={'input-dark'}/>
|
||||
<p className={'input-help'}>
|
||||
This is some descriptive helper text to explain how things work.
|
||||
</p>
|
||||
<div className={'mt-6'}/>
|
||||
<label className={'uppercase text-neutral-200'}>Username</label>
|
||||
<input type={'text'} className={'input-dark error'}/>
|
||||
<p className={'input-help'}>
|
||||
This field has an error.
|
||||
</p>
|
||||
<div className={'mt-6'}/>
|
||||
<label className={'uppercase text-neutral-200'}>Disabled Field</label>
|
||||
<input type={'text'} className={'input-dark'} disabled={true}/>
|
||||
<div className={'mt-6'}/>
|
||||
<label className={'uppercase text-neutral-200'}>Textarea</label>
|
||||
<textarea className={'input-dark h-32'}></textarea>
|
||||
<div className={'mt-6'}/>
|
||||
<button className={'btn btn-primary btn-sm'}>
|
||||
Blue
|
||||
</button>
|
||||
<button className={'btn btn-grey btn-sm ml-2'}>
|
||||
Grey
|
||||
</button>
|
||||
<button className={'btn btn-green btn-sm ml-2'}>
|
||||
Green
|
||||
</button>
|
||||
<button className={'btn btn-red btn-sm ml-2'}>
|
||||
Red
|
||||
</button>
|
||||
<div className={'mt-6'}/>
|
||||
<button className={'btn btn-secondary btn-sm'}>
|
||||
Secondary
|
||||
</button>
|
||||
<button className={'btn btn-secondary btn-red btn-sm ml-2'}>
|
||||
Secondary Danger
|
||||
</button>
|
||||
<div className={'mt-6'}/>
|
||||
<button className={'btn btn-primary btn-lg'}>
|
||||
Large
|
||||
</button>
|
||||
<button className={'btn btn-primary btn-xs ml-2'}>
|
||||
Tiny
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { Form, Formik, FormikActions } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
interface Values {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
email: Yup.string().email().required(),
|
||||
password: Yup.string().required('You must provide your current account password.'),
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const user = useStoreState((state: State<ApplicationStore>) => state.user.data);
|
||||
const updateEmail = useStoreActions((state: Actions<ApplicationStore>) => state.user.updateUserEmail);
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const submit = (values: Values, { resetForm, setSubmitting }: FormikActions<Values>) => {
|
||||
clearFlashes('account:email');
|
||||
|
||||
updateEmail({ ...values })
|
||||
.then(() => addFlash({
|
||||
type: 'success',
|
||||
key: 'account:email',
|
||||
message: 'Your primary email has been updated.',
|
||||
}))
|
||||
.catch(error => addFlash({
|
||||
type: 'error',
|
||||
key: 'account:email',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
}))
|
||||
.then(() => {
|
||||
resetForm();
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
validationSchema={schema}
|
||||
initialValues={{ email: user!.email, password: '' }}
|
||||
>
|
||||
{
|
||||
({ isSubmitting, isValid }) => (
|
||||
<React.Fragment>
|
||||
<SpinnerOverlay large={true} visible={isSubmitting}/>
|
||||
<Form className={'m-0'}>
|
||||
<Field
|
||||
id={'current_email'}
|
||||
type={'email'}
|
||||
name={'email'}
|
||||
label={'Email'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<Field
|
||||
id={'confirm_password'}
|
||||
type={'password'}
|
||||
name={'password'}
|
||||
label={'Confirm Password'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button className={'btn btn-sm btn-primary'} disabled={isSubmitting || !isValid}>
|
||||
Update Email
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
import React from 'react';
|
||||
import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { Form, Formik, FormikActions } from 'formik';
|
||||
import Field from '@/components/elements/Field';
|
||||
import * as Yup from 'yup';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import updateAccountPassword from '@/api/account/updateAccountPassword';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
interface Values {
|
||||
current: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
current: Yup.string().min(1).required('You must provide your current password.'),
|
||||
password: Yup.string().min(8).required(),
|
||||
confirmPassword: Yup.string().test('password', 'Password confirmation does not match the password you entered.', function (value) {
|
||||
return value === this.parent.password;
|
||||
}),
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const user = useStoreState((state: State<ApplicationStore>) => state.user.data);
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const submit = (values: Values, { resetForm, setSubmitting }: FormikActions<Values>) => {
|
||||
clearFlashes('account:password');
|
||||
updateAccountPassword({ ...values })
|
||||
.then(() => {
|
||||
resetForm();
|
||||
addFlash({ key: 'account:password', type: 'success', message: 'Your password has been updated.' });
|
||||
})
|
||||
.catch(error => addFlash({
|
||||
key: 'account:password',
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
}))
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
validationSchema={schema}
|
||||
initialValues={{ current: '', password: '', confirmPassword: '' }}
|
||||
>
|
||||
{
|
||||
({ isSubmitting, isValid }) => (
|
||||
<React.Fragment>
|
||||
<SpinnerOverlay large={true} visible={isSubmitting}/>
|
||||
<Form className={'m-0'}>
|
||||
<Field
|
||||
id={'current_password'}
|
||||
type={'password'}
|
||||
name={'current'}
|
||||
label={'Current Password'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<Field
|
||||
id={'new_password'}
|
||||
type={'password'}
|
||||
name={'password'}
|
||||
label={'New Password'}
|
||||
description={'Your new password should be at least 8 characters in length and unique to this website.'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<Field
|
||||
id={'confirm_password'}
|
||||
type={'password'}
|
||||
name={'confirmPassword'}
|
||||
label={'Confirm New Password'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button className={'btn btn-primary btn-sm'} disabled={isSubmitting || !isValid}>
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
21
resources/scripts/components/elements/ContentBox.tsx
Normal file
21
resources/scripts/components/elements/ContentBox.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
||||
type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
title?: string;
|
||||
borderColor?: string;
|
||||
showFlashes?: string | boolean;
|
||||
}>;
|
||||
|
||||
export default ({ title, borderColor, showFlashes, children, ...props }: Props) => (
|
||||
<div {...props}>
|
||||
{title && <h2 className={'text-neutral-300 mb-4 px-4'}>{title}</h2>}
|
||||
{showFlashes && <FlashMessageRender byKey={typeof showFlashes === 'string' ? showFlashes : undefined}/>}
|
||||
<div className={classNames('bg-neutral-700 p-4 rounded shadow-lg relative', borderColor, {
|
||||
'border-t-4': !!borderColor,
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
41
resources/scripts/components/elements/Field.tsx
Normal file
41
resources/scripts/components/elements/Field.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
import { Field, FieldProps } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
type: string;
|
||||
name: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
validate?: (value: any) => undefined | string | Promise<any>;
|
||||
}
|
||||
|
||||
export default ({ id, type, name, label, description, validate }: Props) => (
|
||||
<Field name={name} validate={validate}>
|
||||
{
|
||||
({ field, form: { errors, touched } }: FieldProps) => (
|
||||
<React.Fragment>
|
||||
{label &&
|
||||
<label htmlFor={id} className={'input-dark-label'}>{label}</label>
|
||||
}
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
{...field}
|
||||
className={classNames('input-dark', {
|
||||
error: touched[field.name] && errors[field.name],
|
||||
})}
|
||||
/>
|
||||
{touched[field.name] && errors[field.name] ?
|
||||
<p className={'input-help'}>
|
||||
{(errors[field.name] as string).charAt(0).toUpperCase() + (errors[field.name] as string).slice(1)}
|
||||
</p>
|
||||
:
|
||||
description ? <p className={'input-help'}>{description}</p> : null
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
</Field>
|
||||
);
|
71
resources/scripts/components/elements/Modal.tsx
Normal file
71
resources/scripts/components/elements/Modal.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
onDismissed: () => void;
|
||||
dismissable?: boolean;
|
||||
closeOnEscape?: boolean;
|
||||
closeOnBackground?: boolean;
|
||||
showSpinnerOverlay?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default (props: Props) => {
|
||||
const [render, setRender] = useState(props.visible);
|
||||
|
||||
const handleEscapeEvent = (e: KeyboardEvent) => {
|
||||
if (props.dismissable !== false && props.closeOnEscape !== false && e.key === 'Escape') {
|
||||
setRender(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => setRender(props.visible), [props.visible]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleEscapeEvent);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleEscapeEvent);
|
||||
}, [render]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
timeout={250}
|
||||
classNames={'fade'}
|
||||
in={render}
|
||||
unmountOnExit={true}
|
||||
onExited={() => props.onDismissed()}
|
||||
>
|
||||
<div className={'modal-mask'} onClick={e => {
|
||||
if (props.dismissable !== false && props.closeOnBackground !== false) {
|
||||
e.stopPropagation();
|
||||
if (e.target === e.currentTarget) {
|
||||
setRender(false);
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<div className={'modal-container top'}>
|
||||
{props.dismissable !== false &&
|
||||
<div className={'modal-close-icon'} onClick={() => setRender(false)}>
|
||||
<FontAwesomeIcon icon={faTimes}/>
|
||||
</div>
|
||||
}
|
||||
{props.showSpinnerOverlay &&
|
||||
<div
|
||||
className={'absolute w-full h-full rounded flex items-center justify-center'}
|
||||
style={{ background: 'hsla(211, 10%, 53%, 0.25)' }}
|
||||
>
|
||||
<Spinner large={false}/>
|
||||
</div>
|
||||
}
|
||||
<div className={'modal-content p-6'}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
11
resources/scripts/components/elements/Spinner.tsx
Normal file
11
resources/scripts/components/elements/Spinner.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default ({ large, centered }: { large?: boolean; centered?: boolean }) => (
|
||||
centered ?
|
||||
<div className={classNames('flex justify-center', { 'm-20': large, 'm-6': !large })}>
|
||||
<div className={classNames('spinner-circle spinner-white', { 'spinner-lg': large })}/>
|
||||
</div>
|
||||
:
|
||||
<div className={classNames('spinner-circle spinner-white', { 'spinner-lg': large })}/>
|
||||
);
|
15
resources/scripts/components/elements/SpinnerOverlay.tsx
Normal file
15
resources/scripts/components/elements/SpinnerOverlay.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
||||
export default ({ large, visible }: { visible: boolean; large?: boolean }) => (
|
||||
<CSSTransition timeout={150} classNames={'fade'} in={visible} unmountOnExit={true}>
|
||||
<div
|
||||
className={classNames('absolute pin-t pin-l flex items-center justify-center w-full h-full rounded')}
|
||||
style={{ background: 'rgba(0, 0, 0, 0.45)' }}
|
||||
>
|
||||
<Spinner large={large}/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
120
resources/scripts/components/server/Console.tsx
Normal file
120
resources/scripts/components/server/Console.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
import React, { createRef } from 'react';
|
||||
import { Terminal } from 'xterm';
|
||||
import * as TerminalFit from 'xterm/lib/addons/fit/fit';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { connect } from 'react-redux';
|
||||
import { Websocket } from '@/plugins/Websocket';
|
||||
import { ServerStore } from '@/state/server';
|
||||
|
||||
const theme = {
|
||||
background: 'transparent',
|
||||
cursor: 'transparent',
|
||||
black: '#000000',
|
||||
red: '#E54B4B',
|
||||
green: '#9ECE58',
|
||||
yellow: '#FAED70',
|
||||
blue: '#396FE2',
|
||||
magenta: '#BB80B3',
|
||||
cyan: '#2DDAFD',
|
||||
white: '#d0d0d0',
|
||||
brightBlack: 'rgba(255, 255, 255, 0.2)',
|
||||
brightRed: '#FF5370',
|
||||
brightGreen: '#C3E88D',
|
||||
brightYellow: '#FFCB6B',
|
||||
brightBlue: '#82AAFF',
|
||||
brightMagenta: '#C792EA',
|
||||
brightCyan: '#89DDFF',
|
||||
brightWhite: '#ffffff',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
connected: boolean;
|
||||
instance: Websocket | null;
|
||||
}
|
||||
|
||||
class Console extends React.PureComponent<Readonly<Props>> {
|
||||
ref = createRef<HTMLDivElement>();
|
||||
terminal = new Terminal({
|
||||
disableStdin: true,
|
||||
cursorStyle: 'underline',
|
||||
allowTransparency: true,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Menlo, Monaco, Consolas, monospace',
|
||||
rows: 30,
|
||||
theme: theme,
|
||||
});
|
||||
|
||||
componentDidMount () {
|
||||
if (this.ref.current) {
|
||||
this.terminal.open(this.ref.current);
|
||||
this.terminal.clear();
|
||||
|
||||
// @see https://github.com/xtermjs/xterm.js/issues/2265
|
||||
// @see https://github.com/xtermjs/xterm.js/issues/2230
|
||||
TerminalFit.fit(this.terminal);
|
||||
}
|
||||
|
||||
if (this.props.connected && this.props.instance) {
|
||||
this.listenForEvents();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps: Readonly<Readonly<Props>>) {
|
||||
if (!prevProps.connected && this.props.connected) {
|
||||
this.listenForEvents();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.props.instance) {
|
||||
this.props.instance.removeListener('server log', this.handleServerLog);
|
||||
this.props.instance.removeListener('server log', this.handleConsoleOutput);
|
||||
}
|
||||
}
|
||||
|
||||
listenForEvents () {
|
||||
const instance = this.props.instance!;
|
||||
|
||||
instance.addListener('server log', this.handleServerLog);
|
||||
instance.addListener('console output', this.handleConsoleOutput);
|
||||
instance.send('send logs');
|
||||
}
|
||||
|
||||
handleServerLog = (lines: string[]) => lines.forEach(data => {
|
||||
return data.split(/\n/g).forEach(line => this.terminal.writeln(line + '\u001b[0m'));
|
||||
});
|
||||
|
||||
handleConsoleOutput = (line: string) => this.terminal.writeln(
|
||||
line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m'
|
||||
);
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={'text-xs font-mono relative'}>
|
||||
<SpinnerOverlay visible={!this.props.connected} large={true}/>
|
||||
<div
|
||||
className={'rounded-t p-2 bg-black overflow-scroll w-full'}
|
||||
style={{
|
||||
minHeight: '16rem',
|
||||
maxHeight: '64rem',
|
||||
}}
|
||||
>
|
||||
<div id={'terminal'} ref={this.ref}/>
|
||||
</div>
|
||||
<div className={'rounded-b bg-neutral-900 text-neutral-100 flex'}>
|
||||
<div className={'flex-no-shrink p-2 font-bold'}>$</div>
|
||||
<div className={'w-full'}>
|
||||
<input type={'text'} className={'bg-transparent text-neutral-100 p-2 pl-0 w-full'}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: ServerStore) => ({
|
||||
connected: state.socket.connected,
|
||||
instance: state.socket.instance,
|
||||
}),
|
||||
)(Console);
|
18
resources/scripts/components/server/ServerConsole.tsx
Normal file
18
resources/scripts/components/server/ServerConsole.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import Console from '@/components/server/Console';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
export default () => {
|
||||
const status = ServerContext.useStoreState(state => state.status.value);
|
||||
|
||||
return (
|
||||
<div className={'my-10 flex'}>
|
||||
<div className={'mx-4 w-3/4 mr-4'}>
|
||||
<Console/>
|
||||
</div>
|
||||
<div className={'flex-1 ml-4'}>
|
||||
<p>Current status: {status}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
34
resources/scripts/components/server/WebsocketHandler.tsx
Normal file
34
resources/scripts/components/server/WebsocketHandler.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Websocket } from '@/plugins/Websocket';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
export default () => {
|
||||
const server = ServerContext.useStoreState(state => state.server.data);
|
||||
const instance = ServerContext.useStoreState(state => state.socket.instance);
|
||||
const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus);
|
||||
const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket);
|
||||
|
||||
useEffect(() => {
|
||||
// If there is already an instance or there is no server, just exit out of this process
|
||||
// since we don't need to make a new connection.
|
||||
if (instance || !server) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Connecting!');
|
||||
|
||||
const socket = new Websocket(
|
||||
`wss://wings.pterodactyl.test:8080/api/servers/${server.uuid}/ws`,
|
||||
'CC8kHCuMkXPosgzGO6d37wvhNcksWxG6kTrA',
|
||||
);
|
||||
|
||||
socket.on('SOCKET_OPEN', () => setConnectionState(true));
|
||||
socket.on('SOCKET_CLOSE', () => setConnectionState(false));
|
||||
socket.on('SOCKET_ERROR', () => setConnectionState(false));
|
||||
socket.on('status', (status) => setServerStatus(status));
|
||||
|
||||
setInstance(socket);
|
||||
}, [ server ]);
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,113 @@
|
|||
import React, { useState } from 'react';
|
||||
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
||||
import Modal from '@/components/elements/Modal';
|
||||
import { Form, Formik, FormikActions } from 'formik';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { object, string } from 'yup';
|
||||
import createServerDatabase from '@/api/server/createServerDatabase';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
||||
interface Values {
|
||||
databaseName: string;
|
||||
connectionsFrom: string;
|
||||
}
|
||||
|
||||
const schema = object().shape({
|
||||
databaseName: string()
|
||||
.required('A database name must be provided.')
|
||||
.min(5, 'Database name must be at least 5 characters.')
|
||||
.max(64, 'Database name must not exceed 64 characters.')
|
||||
.matches(/^[A-Za-z0-9_\-.]{5,64}$/, 'Database name should only contain alphanumeric characters, underscores, dashes, and/or periods.'),
|
||||
connectionsFrom: string()
|
||||
.required('A connection value must be provided.')
|
||||
.matches(/^([1-9]{1,3}|%)(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?$/, 'A valid connection address must be provided.'),
|
||||
});
|
||||
|
||||
export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }) => {
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
const server = ServerContext.useStoreState(state => state.server.data!);
|
||||
|
||||
const submit = (values: Values, { setSubmitting }: FormikActions<Values>) => {
|
||||
clearFlashes();
|
||||
createServerDatabase(server.uuid, { ...values })
|
||||
.then(database => {
|
||||
onCreated(database);
|
||||
setVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
addFlash({
|
||||
key: 'create-database-modal',
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
});
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
initialValues={{ databaseName: '', connectionsFrom: '%' }}
|
||||
validationSchema={schema}
|
||||
>
|
||||
{
|
||||
({ isSubmitting, resetForm }) => (
|
||||
<Modal
|
||||
visible={visible}
|
||||
dismissable={!isSubmitting}
|
||||
showSpinnerOverlay={isSubmitting}
|
||||
onDismissed={() => {
|
||||
resetForm();
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<FlashMessageRender byKey={'create-database-modal'} className={'mb-6'}/>
|
||||
<h3 className={'mb-6'}>Create new database</h3>
|
||||
<Form className={'m-0'}>
|
||||
<Field
|
||||
type={'string'}
|
||||
id={'database_name'}
|
||||
name={'databaseName'}
|
||||
label={'Database Name'}
|
||||
description={'A descriptive name for your database instance.'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<Field
|
||||
type={'string'}
|
||||
id={'connections_from'}
|
||||
name={'connectionsFrom'}
|
||||
label={'Connections From'}
|
||||
description={'Where connections should be allowed from. Use % for wildcards.'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6 text-right'}>
|
||||
<button
|
||||
type={'button'}
|
||||
className={'btn btn-sm btn-secondary mr-2'}
|
||||
onClick={() => setVisible(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button className={'btn btn-sm btn-primary'} type={'submit'}>
|
||||
Create Database
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
<button className={'btn btn-primary btn-lg'} onClick={() => setVisible(true)}>
|
||||
New Database
|
||||
</button>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
159
resources/scripts/components/server/databases/DatabaseRow.tsx
Normal file
159
resources/scripts/components/server/databases/DatabaseRow.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import React, { useState } from 'react';
|
||||
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase';
|
||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
||||
import { faEye } from '@fortawesome/free-solid-svg-icons/faEye';
|
||||
import classNames from 'classnames';
|
||||
import Modal from '@/components/elements/Modal';
|
||||
import { Form, Formik, FormikActions } from 'formik';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { object, string } from 'yup';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import deleteServerDatabase from '@/api/server/deleteServerDatabase';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
|
||||
interface Props {
|
||||
database: ServerDatabase;
|
||||
className?: string;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export default ({ database, className, onDelete }: Props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [connectionVisible, setConnectionVisible] = useState(false);
|
||||
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
const server = ServerContext.useStoreState(state => state.server.data!);
|
||||
|
||||
const schema = object().shape({
|
||||
confirm: string()
|
||||
.required('The database name must be provided.')
|
||||
.oneOf([database.name.split('_', 2)[1], database.name], 'The database name must be provided.'),
|
||||
});
|
||||
|
||||
const submit = (values: { confirm: string }, { setSubmitting }: FormikActions<{ confirm: string }>) => {
|
||||
clearFlashes();
|
||||
deleteServerDatabase(server.uuid, database.id)
|
||||
.then(() => {
|
||||
setVisible(false);
|
||||
setTimeout(() => onDelete(), 150);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
setSubmitting(false);
|
||||
addFlash({
|
||||
key: 'delete-database-modal',
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
initialValues={{ confirm: '' }}
|
||||
validationSchema={schema}
|
||||
>
|
||||
{
|
||||
({ isSubmitting, isValid, resetForm }) => (
|
||||
<Modal
|
||||
visible={visible}
|
||||
dismissable={!isSubmitting}
|
||||
showSpinnerOverlay={isSubmitting}
|
||||
onDismissed={() => { setVisible(false); resetForm(); }}
|
||||
>
|
||||
<FlashMessageRender byKey={'delete-database-modal'} className={'mb-6'}/>
|
||||
<h3 className={'mb-6'}>Confirm database deletion</h3>
|
||||
<p className={'text-sm'}>
|
||||
Deleting a database is a permanent action, it cannot be undone. This will permanetly
|
||||
delete the <strong>{database.name}</strong> database and remove all associated data.
|
||||
</p>
|
||||
<Form className={'m-0 mt-6'}>
|
||||
<Field
|
||||
type={'text'}
|
||||
id={'confirm_name'}
|
||||
name={'confirm'}
|
||||
label={'Confirm Database Name'}
|
||||
description={'Enter the database name to confirm deletion.'}
|
||||
/>
|
||||
<div className={'mt-6 text-right'}>
|
||||
<button
|
||||
type={'button'}
|
||||
className={'btn btn-sm btn-secondary mr-2'}
|
||||
onClick={() => setVisible(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-sm btn-red'}
|
||||
disabled={!isValid}
|
||||
>
|
||||
Delete Database
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
<Modal visible={connectionVisible} onDismissed={() => setConnectionVisible(false)}>
|
||||
<h3 className={'mb-6'}>Database connection details</h3>
|
||||
<div>
|
||||
<label className={'input-dark-label'}>Password</label>
|
||||
<input type={'text'} className={'input-dark'} readOnly={true} value={database.password}/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<label className={'input-dark-label'}>JBDC Connection String</label>
|
||||
<input
|
||||
type={'text'}
|
||||
className={'input-dark'}
|
||||
readOnly={true}
|
||||
value={`jdbc:mysql://${database.username}:${database.password}@${database.connectionString}/${database.name}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6 text-right'}>
|
||||
<button className={'btn btn-sm btn-secondary'} onClick={() => setConnectionVisible(false)}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className={classNames('grey-row-box no-hover', className)}>
|
||||
<div className={'icon'}>
|
||||
<FontAwesomeIcon icon={faDatabase}/>
|
||||
</div>
|
||||
<div className={'flex-1 ml-4'}>
|
||||
<p className={'text-lg'}>{database.name}</p>
|
||||
</div>
|
||||
<div className={'ml-6'}>
|
||||
<p className={'text-center text-xs text-neutral-500 uppercase mb-1 select-none'}>Endpoint:</p>
|
||||
<p className={'text-center text-sm'}>{database.connectionString}</p>
|
||||
</div>
|
||||
<div className={'ml-6'}>
|
||||
<p className={'text-center text-xs text-neutral-500 uppercase mb-1 select-none'}>
|
||||
Connections From:
|
||||
</p>
|
||||
<p className={'text-center text-sm'}>{database.allowConnectionsFrom}</p>
|
||||
</div>
|
||||
<div className={'ml-6'}>
|
||||
<p className={'text-center text-xs text-neutral-500 uppercase mb-1 select-none'}>Username:</p>
|
||||
<p className={'text-center text-sm'}>{database.username}</p>
|
||||
</div>
|
||||
<div className={'ml-6'}>
|
||||
<button className={'btn btn-sm btn-secondary mr-2'} onClick={() => setConnectionVisible(true)}>
|
||||
<FontAwesomeIcon icon={faEye} fixedWidth={true}/>
|
||||
</button>
|
||||
<button className={'btn btn-sm btn-secondary btn-red'} onClick={() => setVisible(true)}>
|
||||
<FontAwesomeIcon icon={faTrashAlt} fixedWidth={true}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import getServerDatabases, { ServerDatabase } from '@/api/server/getServerDatabases';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import DatabaseRow from '@/components/server/databases/DatabaseRow';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseButton';
|
||||
|
||||
export default () => {
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
const [ databases, setDatabases ] = useState<ServerDatabase[]>([]);
|
||||
const server = ServerContext.useStoreState(state => state.server.data!);
|
||||
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('databases');
|
||||
getServerDatabases(server.uuid)
|
||||
.then(databases => {
|
||||
setDatabases(databases);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(error => addFlash({
|
||||
key: 'databases',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
type: 'error',
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={'my-10 mb-6'}>
|
||||
<FlashMessageRender byKey={'databases'}/>
|
||||
{loading ?
|
||||
<Spinner large={true} centered={true}/>
|
||||
:
|
||||
<CSSTransition classNames={'fade'} timeout={250}>
|
||||
<React.Fragment>
|
||||
{databases.length > 0 ?
|
||||
databases.map((database, index) => (
|
||||
<DatabaseRow
|
||||
key={database.id}
|
||||
database={database}
|
||||
onDelete={() => setDatabases(s => [ ...s.filter(d => d.id !== database.id) ])}
|
||||
className={index > 0 ? 'mt-1' : undefined}
|
||||
/>
|
||||
))
|
||||
:
|
||||
<p className={'text-center text-sm text-neutral-200'}>
|
||||
It looks like you have no databases. Click the button below to create one now.
|
||||
</p>
|
||||
}
|
||||
<div className={'mt-6 flex justify-end'}>
|
||||
<CreateDatabaseButton onCreated={database => setDatabases(s => [ ...s, database ])}/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</CSSTransition>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
5
resources/scripts/index.tsx
Normal file
5
resources/scripts/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import App from '@/components/App';
|
||||
|
||||
ReactDOM.render(<App/>, document.getElementById('app'));
|
51
resources/scripts/plugins/Websocket.ts
Normal file
51
resources/scripts/plugins/Websocket.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import Sockette from 'sockette';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export const SOCKET_EVENTS = [
|
||||
'SOCKET_OPEN',
|
||||
'SOCKET_RECONNECT',
|
||||
'SOCKET_CLOSE',
|
||||
'SOCKET_ERROR',
|
||||
];
|
||||
|
||||
export class Websocket extends EventEmitter {
|
||||
socket: Sockette;
|
||||
|
||||
constructor (url: string, protocol: string) {
|
||||
super();
|
||||
|
||||
this.socket = new Sockette(url, {
|
||||
protocols: protocol,
|
||||
onmessage: e => {
|
||||
try {
|
||||
let { event, args } = JSON.parse(e.data);
|
||||
this.emit(event, ...args);
|
||||
} catch (ex) {
|
||||
console.warn('Failed to parse incoming websocket message.', ex);
|
||||
}
|
||||
},
|
||||
onopen: () => this.emit('SOCKET_OPEN'),
|
||||
onreconnect: () => this.emit('SOCKET_RECONNECT'),
|
||||
onclose: () => this.emit('SOCKET_CLOSE'),
|
||||
onerror: () => this.emit('SOCKET_ERROR'),
|
||||
});
|
||||
}
|
||||
|
||||
close (code?: number, reason?: string) {
|
||||
this.socket.close(code, reason);
|
||||
}
|
||||
|
||||
open () {
|
||||
this.socket.open();
|
||||
}
|
||||
|
||||
reconnect () {
|
||||
this.socket.reconnect();
|
||||
}
|
||||
|
||||
send (event: string, payload?: string | string[]) {
|
||||
this.socket.send(JSON.stringify({
|
||||
event, args: Array.isArray(payload) ? payload : [ payload ],
|
||||
}));
|
||||
}
|
||||
}
|
16
resources/scripts/routers/AuthenticationRouter.tsx
Normal file
16
resources/scripts/routers/AuthenticationRouter.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Route, RouteComponentProps } from 'react-router-dom';
|
||||
import LoginContainer from '@/components/auth/LoginContainer';
|
||||
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
|
||||
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
|
||||
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
|
||||
|
||||
export default ({ match }: RouteComponentProps) => (
|
||||
<div className={'mt-32'}>
|
||||
<Route path={`${match.path}/login`} component={LoginContainer} exact/>
|
||||
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
|
||||
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
|
||||
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
|
||||
<Route path={`${match.path}/checkpoint`}/>
|
||||
</div>
|
||||
);
|
22
resources/scripts/routers/DashboardRouter.tsx
Normal file
22
resources/scripts/routers/DashboardRouter.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import DesignElementsContainer from '@/components/dashboard/DesignElementsContainer';
|
||||
import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer';
|
||||
import NavigationBar from '@/components/NavigationBar';
|
||||
import DashboardContainer from '@/components/dashboard/DashboardContainer';
|
||||
import TransitionRouter from '@/TransitionRouter';
|
||||
|
||||
export default ({ location }: RouteComponentProps) => (
|
||||
<React.Fragment>
|
||||
<NavigationBar/>
|
||||
<TransitionRouter>
|
||||
<div className={'w-full mx-auto'} style={{ maxWidth: '1200px' }}>
|
||||
<Switch location={location}>
|
||||
<Route path={'/'} component={DashboardContainer} exact/>
|
||||
<Route path={'/account'} component={AccountOverviewContainer}/>
|
||||
<Route path={'/design'} component={DesignElementsContainer}/>
|
||||
</Switch>
|
||||
</div>
|
||||
</TransitionRouter>
|
||||
</React.Fragment>
|
||||
);
|
63
resources/scripts/routers/ServerRouter.tsx
Normal file
63
resources/scripts/routers/ServerRouter.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import NavigationBar from '@/components/NavigationBar';
|
||||
import ServerConsole from '@/components/server/ServerConsole';
|
||||
import TransitionRouter from '@/TransitionRouter';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import WebsocketHandler from '@/components/server/WebsocketHandler';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { Provider } from 'react-redux';
|
||||
import DatabasesContainer from '@/components/server/databases/DatabasesContainer';
|
||||
|
||||
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
||||
const server = ServerContext.useStoreState(state => state.server.data);
|
||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
|
||||
|
||||
if (!server) {
|
||||
getServer(match.params.id);
|
||||
}
|
||||
|
||||
useEffect(() => () => clearServerState(), []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NavigationBar/>
|
||||
<div id={'sub-navigation'}>
|
||||
<div className={'mx-auto'} style={{ maxWidth: '1200px' }}>
|
||||
<div className={'items'}>
|
||||
<NavLink to={`${match.url}`} exact>Console</NavLink>
|
||||
<NavLink to={`${match.url}/files`}>File Manager</NavLink>
|
||||
<NavLink to={`${match.url}/databases`}>Databases</NavLink>
|
||||
<NavLink to={`${match.url}/users`}>User Management</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Provider store={ServerContext.useStore()}>
|
||||
<TransitionRouter>
|
||||
<div className={'w-full mx-auto'} style={{ maxWidth: '1200px' }}>
|
||||
{!server ?
|
||||
<div className={'flex justify-center m-20'}>
|
||||
<Spinner large={true}/>
|
||||
</div>
|
||||
:
|
||||
<React.Fragment>
|
||||
<WebsocketHandler/>
|
||||
<Switch location={location}>
|
||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||
<Route path={`${match.path}/databases`} component={DatabasesContainer}/>
|
||||
</Switch>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
</TransitionRouter>
|
||||
</Provider>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default (props: RouteComponentProps<any>) => (
|
||||
<ServerContext.Provider>
|
||||
<ServerRouter {...props}/>
|
||||
</ServerContext.Provider>
|
||||
);
|
28
resources/scripts/state/flashes.ts
Normal file
28
resources/scripts/state/flashes.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Action, action } from 'easy-peasy';
|
||||
import { FlashMessageType } from '@/components/MessageBox';
|
||||
|
||||
export interface FlashStore {
|
||||
items: FlashMessage[];
|
||||
addFlash: Action<FlashStore, FlashMessage>;
|
||||
clearFlashes: Action<FlashStore, string | void>;
|
||||
}
|
||||
|
||||
export interface FlashMessage {
|
||||
id?: string;
|
||||
key?: string;
|
||||
type: FlashMessageType;
|
||||
title?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const flashes: FlashStore = {
|
||||
items: [],
|
||||
addFlash: action((state, payload) => {
|
||||
state.items.push(payload);
|
||||
}),
|
||||
clearFlashes: action((state, payload) => {
|
||||
state.items = payload ? state.items.filter(flashes => flashes.key !== payload) : [];
|
||||
}),
|
||||
};
|
||||
|
||||
export default flashes;
|
15
resources/scripts/state/index.ts
Normal file
15
resources/scripts/state/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { createStore } from 'easy-peasy';
|
||||
import flashes, { FlashStore } from '@/state/flashes';
|
||||
import user, { UserStore } from '@/state/user';
|
||||
|
||||
export interface ApplicationStore {
|
||||
flashes: FlashStore;
|
||||
user: UserStore;
|
||||
}
|
||||
|
||||
const state: ApplicationStore = {
|
||||
flashes,
|
||||
user,
|
||||
};
|
||||
|
||||
export const store = createStore(state);
|
57
resources/scripts/state/server/index.ts
Normal file
57
resources/scripts/state/server/index.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import getServer, { Server } from '@/api/server/getServer';
|
||||
import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy';
|
||||
import socket, { SocketStore } from './socket';
|
||||
|
||||
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
|
||||
|
||||
interface ServerDataStore {
|
||||
data?: Server;
|
||||
getServer: Thunk<ServerDataStore, string, {}, any, Promise<void>>;
|
||||
setServer: Action<ServerDataStore, Server>;
|
||||
}
|
||||
|
||||
const server: ServerDataStore = {
|
||||
getServer: thunk(async (actions, payload) => {
|
||||
const server = await getServer(payload);
|
||||
actions.setServer(server);
|
||||
}),
|
||||
setServer: action((state, payload) => {
|
||||
state.data = payload;
|
||||
}),
|
||||
};
|
||||
|
||||
interface ServerStatusStore {
|
||||
value: ServerStatus;
|
||||
setServerStatus: Action<ServerStatusStore, ServerStatus>;
|
||||
}
|
||||
|
||||
const status: ServerStatusStore = {
|
||||
value: 'offline',
|
||||
setServerStatus: action((state, payload) => {
|
||||
state.value = payload;
|
||||
}),
|
||||
};
|
||||
|
||||
export interface ServerStore {
|
||||
server: ServerDataStore;
|
||||
socket: SocketStore;
|
||||
status: ServerStatusStore;
|
||||
clearServerState: Action<ServerStore>;
|
||||
}
|
||||
|
||||
export const ServerContext = createContextStore<ServerStore>({
|
||||
server,
|
||||
socket,
|
||||
status,
|
||||
clearServerState: action(state => {
|
||||
state.server.data = undefined;
|
||||
|
||||
if (state.socket.instance) {
|
||||
state.socket.instance.removeAllListeners();
|
||||
state.socket.instance.close();
|
||||
}
|
||||
|
||||
state.socket.instance = null;
|
||||
state.socket.connected = false;
|
||||
}),
|
||||
}, { name: 'ServerStore' });
|
22
resources/scripts/state/server/socket.ts
Normal file
22
resources/scripts/state/server/socket.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Action, action } from 'easy-peasy';
|
||||
import { Websocket } from '@/plugins/Websocket';
|
||||
|
||||
export interface SocketStore {
|
||||
instance: Websocket | null;
|
||||
connected: boolean;
|
||||
setInstance: Action<SocketStore, Websocket | null>;
|
||||
setConnectionState: Action<SocketStore, boolean>;
|
||||
}
|
||||
|
||||
const socket: SocketStore = {
|
||||
instance: null,
|
||||
connected: false,
|
||||
setInstance: action((state, payload) => {
|
||||
state.instance = payload;
|
||||
}),
|
||||
setConnectionState: action((state, payload) => {
|
||||
state.connected = payload;
|
||||
}),
|
||||
};
|
||||
|
||||
export default socket;
|
41
resources/scripts/state/user.ts
Normal file
41
resources/scripts/state/user.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Action, action, Thunk, thunk } from 'easy-peasy';
|
||||
import updateAccountEmail from '@/api/account/updateAccountEmail';
|
||||
|
||||
export interface UserData {
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
language: string;
|
||||
rootAdmin: boolean;
|
||||
useTotp: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface UserStore {
|
||||
data?: UserData;
|
||||
setUserData: Action<UserStore, UserData>;
|
||||
updateUserData: Action<UserStore, Partial<UserData>>;
|
||||
updateUserEmail: Thunk<UserStore, { email: string; password: string }, any, {}, Promise<void>>;
|
||||
}
|
||||
|
||||
const user: UserStore = {
|
||||
data: undefined,
|
||||
setUserData: action((state, payload) => {
|
||||
state.data = payload;
|
||||
}),
|
||||
|
||||
updateUserData: action((state, payload) => {
|
||||
// Limitation of Typescript, can't do much about that currently unfortunately.
|
||||
// @ts-ignore
|
||||
state.data = { ...state.data, ...payload };
|
||||
}),
|
||||
|
||||
updateUserEmail: thunk(async (actions, payload) => {
|
||||
await updateAccountEmail(payload.email, payload.password);
|
||||
|
||||
actions.updateUserData({ email: payload.email });
|
||||
}),
|
||||
};
|
||||
|
||||
export default user;
|
28
resources/styles/components/animations.css
Normal file
28
resources/styles/components/animations.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*! purgecss start ignore */
|
||||
.fade-enter {
|
||||
@apply .opacity-0;
|
||||
}
|
||||
|
||||
.fade-enter-active {
|
||||
@apply .opacity-100;
|
||||
transition: opacity 250ms;
|
||||
}
|
||||
|
||||
.fade-exit {
|
||||
@apply .opacity-100;
|
||||
}
|
||||
|
||||
.fade-exit-active {
|
||||
@apply .opacity-0;
|
||||
transition: opacity 250ms;
|
||||
}
|
||||
|
||||
/** @todo fix this, hides footer stuff */
|
||||
div.route-transition-group {
|
||||
@apply .relative;
|
||||
|
||||
& section {
|
||||
@apply .absolute .w-full .pin-t .pin-l;
|
||||
}
|
||||
}
|
||||
/*! purgecss end ignore */
|
9
resources/styles/components/authentication.css
Normal file
9
resources/styles/components/authentication.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
.login-box {
|
||||
@apply .bg-white .shadow-lg .rounded-lg .p-6;
|
||||
|
||||
@screen xsx {
|
||||
@apply .rounded-none;
|
||||
margin-top: 25%;
|
||||
box-shadow: 0 15px 30px 0 rgba(0, 0, 0, .2), 0 -15px 30px 0 rgba(0, 0, 0, .2);
|
||||
}
|
||||
}
|
81
resources/styles/components/filemanager.css
Normal file
81
resources/styles/components/filemanager.css
Normal file
|
@ -0,0 +1,81 @@
|
|||
.filemanager {
|
||||
& .header {
|
||||
@apply .flex .text-xs .text-neutral-600 .pb-4 .font-bold .border-b .border-neutral-200 .mb-3 .uppercase;
|
||||
|
||||
& > div:not(:last-of-type) {
|
||||
@apply .pr-4;
|
||||
}
|
||||
}
|
||||
|
||||
& .row {
|
||||
@apply .flex .text-sm .py-3 .text-sm .border .border-transparent .text-black .rounded .no-underline;
|
||||
|
||||
& > div:not(:last-of-type) {
|
||||
@apply .pr-4;
|
||||
}
|
||||
|
||||
&.active-selection, &:hover {
|
||||
@apply .bg-neutral-50 .text-neutral-900;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
@apply .w-8 .text-center;
|
||||
|
||||
& > svg {
|
||||
@apply .h-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
@apply .absolute .bg-white .py-2 .border .border-neutral-300 .shadow-lg .rounded .text-neutral-600 .text-sm .cursor-pointer;
|
||||
|
||||
& > div:not(:last-of-type) {
|
||||
@apply .border-b .border-neutral-100 .pb-2 .mb-2;
|
||||
}
|
||||
|
||||
& .context-row {
|
||||
@apply .flex .flex-row .items-center .py-2 .px-8 .mx-2 .rounded;
|
||||
transition: background-color 50ms linear;
|
||||
|
||||
& > .icon {
|
||||
@apply .flex-none;
|
||||
|
||||
& > svg {
|
||||
@apply .h-4;
|
||||
}
|
||||
}
|
||||
|
||||
& > .action {
|
||||
@apply .flex-auto .pl-2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply .bg-neutral-50 .text-neutral-800;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@apply .border .border-transparent;
|
||||
transition: border 50ms linear;
|
||||
|
||||
&:hover {
|
||||
@apply .bg-red-50 .border-red-100;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filemanager-breadcrumbs {
|
||||
@apply .px-4 .py-3 .mb-6 .rounded .bg-white .text-neutral-400 .border .border-neutral-100 .shadow;
|
||||
|
||||
& a {
|
||||
@apply .no-underline .text-neutral-400;
|
||||
transition: color 100ms linear;
|
||||
|
||||
&:hover {
|
||||
@apply .text-primary-500;
|
||||
}
|
||||
}
|
||||
}
|
197
resources/styles/components/forms.css
Normal file
197
resources/styles/components/forms.css
Normal file
|
@ -0,0 +1,197 @@
|
|||
textarea, select, input, button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type=number]::-webkit-outer-spin-button,
|
||||
input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling for other forms throughout the Panel.
|
||||
*/
|
||||
.input, .input-dark {
|
||||
@apply .appearance-none .w-full;
|
||||
min-width: 0;
|
||||
|
||||
&:required, &:invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply .p-3 .rounded .border .border-neutral-200 .text-neutral-800;
|
||||
transition: border 150ms linear;
|
||||
|
||||
&:focus {
|
||||
@apply .border-primary-400;
|
||||
}
|
||||
|
||||
&.error {
|
||||
@apply .text-red-600 .border-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
@apply .bg-neutral-100 .border-neutral-200;
|
||||
}
|
||||
|
||||
.input + .input-help {
|
||||
@apply .text-xs .text-neutral-400 .pt-2;
|
||||
|
||||
&.error {
|
||||
@apply .text-red-600;
|
||||
}
|
||||
}
|
||||
|
||||
.input-dark {
|
||||
@apply .p-3 .bg-neutral-600 .border .border-neutral-500 .text-sm .rounded .text-neutral-200 .shadow-none;
|
||||
transition: border 150ms linear, box-shaodw 150ms ease-in;
|
||||
|
||||
&:focus {
|
||||
@apply .shadow-md .border-neutral-400;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply .border-neutral-400;
|
||||
}
|
||||
|
||||
& + .input-help {
|
||||
@apply .text-xs .text-neutral-400 .mt-2
|
||||
}
|
||||
|
||||
&.error {
|
||||
@apply .text-red-100 .border-red-400;
|
||||
}
|
||||
|
||||
&.error + .input-help {
|
||||
@apply .text-red-400;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply .opacity-75;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
@apply .block .text-xs .uppercase .text-neutral-700 .mb-2;
|
||||
}
|
||||
|
||||
select:not(.appearance-none) {
|
||||
@apply .outline-none .appearance-none .block .bg-white .border .border-neutral-200 .text-neutral-400 .p-3 .pr-8 rounded;
|
||||
transition: border-color 150ms linear, color 150ms linear;
|
||||
|
||||
&:hover:not(:disabled), &:focus {
|
||||
@apply .outline-none .border-primary-500 .text-neutral-700;
|
||||
}
|
||||
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
|
||||
&::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
background: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z'/%3e%3c/svg%3e ") no-repeat center center;
|
||||
background-size: 1rem;
|
||||
background-position-x: calc(100% - 0.75rem);
|
||||
}
|
||||
|
||||
.input-dark-label {
|
||||
@apply .uppercase .text-neutral-200;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
@apply .block .uppercase .tracking-wide .text-neutral-800 .text-xs .font-bold;
|
||||
|
||||
&:not(.mb-0) {
|
||||
@apply .mb-2;
|
||||
}
|
||||
}
|
||||
|
||||
a.btn {
|
||||
@apply .no-underline;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply .rounded .p-2 .uppercase .tracking-wide .text-sm;
|
||||
transition: all 150ms linear;
|
||||
|
||||
/**
|
||||
* Button Colors
|
||||
*/
|
||||
&.btn-primary {
|
||||
@apply .bg-primary-500 .border-primary-600 .border .text-primary-50;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@apply .bg-primary-600 .border-primary-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-green {
|
||||
@apply .bg-green-500 .border-green-600 .border .text-green-50;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@apply .bg-green-600 .border-green-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-red {
|
||||
&:not(.btn-secondary) {
|
||||
@apply .bg-red-500 .border-red-600 .text-red-50;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@apply .bg-red-600 .border-red-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-grey {
|
||||
@apply .border .border-neutral-600 .bg-neutral-500 .text-neutral-50;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@apply .bg-neutral-600 .border-neutral-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
@apply .border .border-neutral-600 .bg-transparent .text-neutral-200;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@apply .border-neutral-500 .text-neutral-100;
|
||||
}
|
||||
|
||||
&.btn-red:hover:not(:disabled) {
|
||||
@apply .bg-red-500 .border-red-600 .text-red-50;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Button Sizes
|
||||
*/
|
||||
&.btn-jumbo {
|
||||
@apply .p-4 .w-full;
|
||||
}
|
||||
|
||||
&.btn-lg {
|
||||
@apply .p-4 .text-sm;
|
||||
}
|
||||
|
||||
&.btn-sm {
|
||||
@apply .p-3;
|
||||
}
|
||||
|
||||
&.btn-xs {
|
||||
@apply .p-2 .text-xs;
|
||||
}
|
||||
|
||||
&:disabled, &.disabled {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
21
resources/styles/components/miscellaneous.css
Normal file
21
resources/styles/components/miscellaneous.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
code.clean {
|
||||
@apply .font-mono .px-2 .py-1;
|
||||
background-color: #eef1f6;
|
||||
color: #596981;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.grey-row-box {
|
||||
@apply .flex .rounded .no-underline .text-neutral-200 .items-center .bg-neutral-700 .p-4 .border .border-transparent;
|
||||
transition: border-color 150ms linear;
|
||||
|
||||
&:not(.no-hover):hover {
|
||||
@apply .border-neutral-500;
|
||||
}
|
||||
|
||||
& > div.icon {
|
||||
@apply .rounded-full .bg-neutral-500 .p-3;
|
||||
}
|
||||
}
|
50
resources/styles/components/modal.css
Normal file
50
resources/styles/components/modal.css
Normal file
|
@ -0,0 +1,50 @@
|
|||
.modal-mask {
|
||||
@apply .fixed .pin .z-50 .overflow-auto .flex;
|
||||
background: rgba(0, 0, 0, 0.70);
|
||||
transition: opacity 250ms ease;
|
||||
|
||||
& > .modal-container {
|
||||
@apply .relative .w-full .max-w-md .m-auto .flex-col .flex;
|
||||
|
||||
&.top {
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
& > .modal-close-icon {
|
||||
@apply .absolute .pin-r .p-2 .text-white .cursor-pointer .opacity-50;
|
||||
transition: opacity 150ms linear, transform 150ms ease-in;
|
||||
top: -2rem;
|
||||
|
||||
&:hover {
|
||||
@apply .opacity-100;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
& > .modal-content {
|
||||
@apply .bg-neutral-800 .rounded .shadow-md;
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
/**
|
||||
* On tiny phone screens make sure there is a margin on the sides and also
|
||||
* center the modal rather than putting it towards the top of the screen.
|
||||
*/
|
||||
@screen smx {
|
||||
width: 90%;
|
||||
.top {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .modal-container.full-screen {
|
||||
@apply .w-3/4 .mt-32;
|
||||
height: calc(100vh - 16rem);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
& > .modal-container.w-auto {
|
||||
@apply .w-auto;
|
||||
}
|
||||
}
|
64
resources/styles/components/navigation.css
Normal file
64
resources/styles/components/navigation.css
Normal file
|
@ -0,0 +1,64 @@
|
|||
#navigation {
|
||||
@apply .w-full .bg-neutral-900 .shadow-md;
|
||||
|
||||
& > div {
|
||||
@apply .mx-auto .w-full .flex .items-center;
|
||||
}
|
||||
|
||||
& #logo {
|
||||
@apply .flex-1;
|
||||
|
||||
& > a {
|
||||
@apply .text-2xl .font-header .px-4 .no-underline .text-neutral-200;
|
||||
transition: color 150ms linear;
|
||||
|
||||
&:hover {
|
||||
@apply .text-neutral-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .right-navigation {
|
||||
@apply .flex .h-full .items-center .justify-center;
|
||||
|
||||
& > a {
|
||||
@apply .flex .items-center .h-full .no-underline .text-neutral-300 .px-6;
|
||||
transition: background-color 150ms linear, color 150ms linear, box-shadow 150ms ease-in;
|
||||
|
||||
&.active, &:hover {
|
||||
@apply .text-neutral-100 .bg-black;
|
||||
box-shadow: inset 0 -2px config('colors.cyan-700');
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: inset 0 -2px config('colors.cyan-500');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sub-navigation {
|
||||
@apply .w-full .bg-neutral-700 .shadow;
|
||||
|
||||
.items {
|
||||
@apply .flex .items-center .text-sm .mx-2;
|
||||
|
||||
& > a, & > div {
|
||||
@apply .inline-block .py-3 .px-4 .text-neutral-300 .no-underline;
|
||||
transition: color 150ms linear, box-shadow 150ms ease-in;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
@apply .ml-2;
|
||||
}
|
||||
|
||||
&.active, &:hover {
|
||||
@apply .text-neutral-100;
|
||||
box-shadow: inset 0 -2px config('colors.cyan-700');
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: inset 0 -2px config('colors.cyan-500');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
resources/styles/components/notifications.css
Normal file
46
resources/styles/components/notifications.css
Normal file
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Styling to control alert boxes.
|
||||
*/
|
||||
.alert {
|
||||
@apply .p-2 .border .items-center .leading-normal .rounded .flex .w-full .text-sm;
|
||||
|
||||
& > .title {
|
||||
@apply .flex .rounded-full .uppercase .px-2 .py-1 .text-xs .font-bold .mr-3 .leading-none;
|
||||
}
|
||||
|
||||
& > .message {
|
||||
@apply .mr-2 .text-left .flex-auto;
|
||||
}
|
||||
|
||||
&.error {
|
||||
@apply .bg-red-600 .border-red-800 .text-red-50;
|
||||
|
||||
& > .title {
|
||||
@apply .bg-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
@apply .bg-primary-600 .border-primary-800 .text-primary-50;
|
||||
|
||||
& > .title {
|
||||
@apply .bg-primary-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply .bg-green-600 .border-green-800 .text-green-50;
|
||||
|
||||
& > .title {
|
||||
@apply .bg-green-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply .bg-yellow-600 .border-yellow-800 .text-yellow-50;
|
||||
|
||||
& > .title {
|
||||
@apply .bg-yellow-500;
|
||||
}
|
||||
}
|
||||
}
|
91
resources/styles/components/spinners.css
Normal file
91
resources/styles/components/spinners.css
Normal file
|
@ -0,0 +1,91 @@
|
|||
.spinner {
|
||||
@apply .h-4 .relative .bg-transparent;
|
||||
pointer-events: none;
|
||||
|
||||
&.spinner-xl {
|
||||
@apply .h-16;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@apply .border-2 .border-neutral-400 .absolute .block .h-4 .w-4 .rounded-full;
|
||||
animation: spinners--spin 500ms infinite linear;
|
||||
border-top-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
content: '';
|
||||
left: calc(50% - (1em / 2));
|
||||
}
|
||||
|
||||
&.spinner-relative:after {
|
||||
@apply .relative;
|
||||
}
|
||||
|
||||
&.spinner-xl:after {
|
||||
@apply .h-16 .w-16;
|
||||
left: calc(50% - (4rem / 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Speeds
|
||||
*/
|
||||
&.spin-slow:after {
|
||||
animation: spinners--spin 1200ms infinite linear;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spinner Colors
|
||||
*/
|
||||
&.blue:after, &.text-blue:after {
|
||||
@apply .border-primary-500;
|
||||
}
|
||||
|
||||
&.white:after, &.text-white:after {
|
||||
@apply .border-white;
|
||||
}
|
||||
|
||||
&.spinner-thick:after {
|
||||
@apply .border-4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinners--spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-circle {
|
||||
@apply .w-8 .h-8;
|
||||
border: 3px solid hsla(211, 12%, 43%, 0.2);
|
||||
border-top-color: hsl(211, 12%, 43%);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s cubic-bezier(0.55, 0.25, 0.25, 0.70) infinite;
|
||||
|
||||
&.spinner-sm {
|
||||
@apply .w-4 .h-4 .border-2;
|
||||
}
|
||||
|
||||
&.spinner-lg {
|
||||
@apply .w-16 .h-16;
|
||||
border-width: 6px;
|
||||
}
|
||||
|
||||
&.spinner-blue {
|
||||
border: 3px solid hsla(212, 92%, 43%, 0.2);
|
||||
border-top-color: hsl(212, 92%, 43%);
|
||||
}
|
||||
|
||||
&.spinner-white {
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
17
resources/styles/components/typography.css
Normal file
17
resources/styles/components/typography.css
Normal file
|
@ -0,0 +1,17 @@
|
|||
@import url('//fonts.googleapis.com/css?family=Rubik:300,400,500&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css?family=IBM+Plex+Sans:500&display=swap');
|
||||
|
||||
body {
|
||||
@apply .text-neutral-200;
|
||||
letter-spacing: 0.015em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply .font-medium;
|
||||
letter-spacing: 0;
|
||||
font-family: 'IBM Plex Sans', -apple-system, '"Roboto"', 'system-ui', 'sans-serif';
|
||||
}
|
||||
|
||||
p {
|
||||
@apply .text-neutral-200 .leading-snug;
|
||||
}
|
33
resources/styles/main.css
Normal file
33
resources/styles/main.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Tailwind Preflight Classes
|
||||
*/
|
||||
@import "tailwindcss/preflight";
|
||||
@import "tailwindcss/components";
|
||||
|
||||
@import "xterm/src/xterm.css";
|
||||
|
||||
/**
|
||||
* Pterodactyl Specific CSS
|
||||
*/
|
||||
@import "components/typography.css";
|
||||
@import "components/animations.css";
|
||||
@import "components/authentication.css";
|
||||
@import "components/forms.css";
|
||||
@import "components/miscellaneous.css";
|
||||
@import "components/modal.css";
|
||||
@import "components/navigation.css";
|
||||
@import "components/notifications.css";
|
||||
@import "components/spinners.css";
|
||||
@import "components/filemanager.css";
|
||||
|
||||
/**
|
||||
* Tailwind Utilities
|
||||
*/
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
/**
|
||||
* Assorted Other CSS
|
||||
*/
|
||||
body {
|
||||
@apply .font-sans;
|
||||
}
|
|
@ -81,7 +81,7 @@
|
|||
<div class="form-group">
|
||||
<label class="form-label">Input Rules</label>
|
||||
<input type="text" name="rules" class="form-control" value="{{ $variable->rules }}" />
|
||||
<p class="text-muted small">These rules are defined using standard Laravel Framework validation rules.</p>
|
||||
<p class="text-muted small">These rules are defined using standard <a href="https://laravel.com/docs/5.7/validation#available-validation-rules" target="_blank">Laravel Framework validation rules</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
|
@ -134,7 +134,7 @@
|
|||
<div class="form-group">
|
||||
<label class="control-label">Input Rules <span class="field-required"></span></label>
|
||||
<input type="text" name="rules" class="form-control" value="{{ old('rules', 'required|string|max:20') }}" placeholder="required|string|max:20" />
|
||||
<p class="text-muted small">These rules are defined using standard Laravel Framework validation rules.</p>
|
||||
<p class="text-muted small">These rules are defined using standard <a href="https://laravel.com/docs/5.7/validation#available-validation-rules" target="_blank">Laravel Framework validation rules</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
<div class="row">
|
||||
@if($node->maintenance_mode)
|
||||
<div class="col-sm-12">
|
||||
<div class="info-box bg-orange">
|
||||
<div class="info-box bg-grey">
|
||||
<span class="info-box-icon"><i class="ion ion-wrench"></i></span>
|
||||
<div class="info-box-content" style="padding: 23px 10px 0;">
|
||||
<span class="info-box-text">This node is under</span>
|
||||
|
@ -169,4 +169,4 @@
|
|||
});
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<th class="text-center">CPU</th>
|
||||
<th class="text-center">Status</th>
|
||||
</tr>
|
||||
@foreach($node->servers as $server)
|
||||
@foreach($servers as $server)
|
||||
<tr data-server="{{ $server->uuid }}">
|
||||
<td><code>{{ $server->uuidShort }}</code></td>
|
||||
<td><a href="{{ route('admin.servers.view', $server->id) }}">{{ $server->name }}</a></td>
|
||||
|
@ -64,6 +64,11 @@
|
|||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
@if($servers->hasPages())
|
||||
<div class="box-footer with-border">
|
||||
<div class="col-md-12 text-center">{!! $servers->render() !!}</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue