Merge pull request #880 from pelican-dev/feature/vite

Remove old client area and switch to vite
This commit is contained in:
Scai 2025-01-07 02:06:27 +02:00 committed by GitHub
commit 6707d1ccf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
335 changed files with 1235 additions and 26042 deletions

View File

@ -1,6 +0,0 @@
public
node_modules
resources/views
babel.config.js
tailwind.config.js
webpack.config.js

View File

@ -1,52 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
jsx: true,
},
project: './tsconfig.json',
tsconfigRootDir: './',
},
settings: {
react: {
pragma: 'React',
version: 'detect',
},
linkComponents: [
{ name: 'Link', linkAttribute: 'to' },
{ name: 'NavLink', linkAttribute: 'to' },
],
},
env: {
browser: true,
es6: true,
},
plugins: ['react', 'react-hooks', 'prettier', '@typescript-eslint'],
extends: [
// 'standard',
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest-dom/recommended',
],
rules: {
eqeqeq: 'error',
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
// TypeScript can infer this significantly better than eslint ever can.
'react/prop-types': 0,
'react/display-name': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-non-null-assertion': 0,
// 'react/no-unknown-property': ['error', { ignore: ['css'] }],
// This setup is required to avoid a spam of errors when running eslint about React being
// used before it is defined.
//
// @see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
'no-use-before-define': 0,
'@typescript-eslint/no-use-before-define': 'warn',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }],
},
};

View File

@ -3,10 +3,10 @@ name: Build
on:
push:
branches:
- '**'
- "**"
pull_request:
branches:
- '**'
- "**"
jobs:
ui:
@ -20,14 +20,25 @@ jobs:
- name: Code Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install PHP dependencies
run: composer install --no-interaction --no-suggest --prefer-dist
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- name: Install dependencies
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn build:production
run: yarn build

View File

@ -11,22 +11,33 @@ jobs:
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Code checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install PHP dependencies
run: composer install --no-interaction --no-suggest --prefer-dist
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: "yarn"
- name: Install dependencies
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn build:production
run: yarn build
- name: Create release branch and bump version
env:

View File

@ -244,7 +244,7 @@ class Handler extends ExceptionHandler
return new JsonResponse($this->convertExceptionToArray($exception), JsonResponse::HTTP_UNAUTHORIZED);
}
return redirect()->guest('/auth/login');
return redirect()->guest(route('filament.app.auth.login'));
}
/**

View File

@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use Illuminate\Http\Request;
use Illuminate\Auth\AuthManager;
@ -18,7 +19,7 @@ class RedirectIfAuthenticated
public function handle(Request $request, \Closure $next, ?string $guard = null): mixed
{
if ($this->authManager->guard($guard)->check()) {
return redirect()->route('index');
return redirect(ListServers::getUrl());
}
return $next($request);

View File

@ -1,27 +0,0 @@
<?php
namespace App\Http\ViewComposers;
use App\Services\Helpers\AssetHashService;
use Illuminate\View\View;
readonly class AssetComposer
{
public function __construct(private AssetHashService $assetHashService) {}
public function compose(View $view): void
{
$view->with('asset', $this->assetHashService);
$view->with('siteConfiguration', [
'name' => config('app.name', 'Panel'),
'locale' => config('app.locale') ?? 'en',
'recaptcha' => [
'enabled' => config('turnstile.turnstile_enabled', false),
'siteKey' => config('turnstile.turnstile_site_key') ?? '',
],
'usesSyncDriver' => config('queue.default') === 'sync',
'serverDescriptionsEditable' => config('panel.editable_server_descriptions'),
]);
}
}

View File

@ -201,7 +201,7 @@ class Node extends Model
],
],
'allowed_mounts' => $this->mounts->pluck('source')->toArray(),
'remote' => route('index'),
'remote' => route('filament.app.resources...index'),
];
}

View File

@ -3,6 +3,7 @@
namespace App\Notifications;
use App\Events\Server\SubUserRemoved;
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use App\Models\Server;
use App\Models\User;
use Illuminate\Bus\Queueable;
@ -52,6 +53,6 @@ class RemovedFromServer extends Notification implements ShouldQueue
->greeting('Hello ' . $this->user->username . '.')
->line('You have been removed as a subuser for the following server.')
->line('Server Name: ' . $this->server->name)
->action('Visit Panel', route('index'));
->action('Visit Panel', ListServers::getUrl());
}
}

View File

@ -7,6 +7,7 @@ use Illuminate\Bus\Queueable;
use App\Models\Server;
use Illuminate\Container\Container;
use App\Events\Server\Installed;
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Notifications\Dispatcher;
@ -63,6 +64,6 @@ class ServerInstalled extends Notification implements ShouldQueue
->greeting('Hello ' . $this->user->username . '.')
->line('Your server has finished installing and is now ready for you to use.')
->line('Server Name: ' . $this->server->name)
->action('Login and Begin Using', route('index'));
->action('Login and Begin Using', ListServers::getUrl());
}
}

View File

@ -21,6 +21,7 @@ use Filament\View\PanelsRenderHook;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Http;
@ -106,12 +107,28 @@ class AppServiceProvider extends ServiceProvider
'warning' => Color::Amber,
]);
FilamentView::registerRenderHook(
PanelsRenderHook::HEAD_START,
fn (): string => Blade::render(<<<'HTML'
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
HTML),
);
FilamentView::registerRenderHook(
PanelsRenderHook::CONTENT_START,
fn () => view('filament.server-conflict-banner'),
scopes: Console::class,
);
FilamentView::registerRenderHook(
PanelsRenderHook::BODY_END,
fn (): string => Blade::render(<<<'HTML'
@livewireScripts
@vite(['resources/js/app.js'])
HTML),
);
// Don't run any health checks during tests
if (!$app->runningUnitTests()) {
Health::checks([

View File

@ -25,7 +25,6 @@ class AppPanelProvider extends PanelProvider
{
return $panel
->id('app')
->path('app')
->spa()
->breadcrumbs(false)
->brandName(config('app.name', 'Pelican'))

View File

@ -31,7 +31,7 @@ class ServerPanelProvider extends PanelProvider
return $panel
->id('server')
->path('app/server')
->homeUrl('/app')
->homeUrl('/')
->spa()
->tenant(Server::class)
->brandName(config('app.name', 'Pelican'))

View File

@ -1,17 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Http\ViewComposers\AssetComposer;
class ViewComposerServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*/
public function boot(): void
{
$this->app->make('view')->composer('*', AssetComposer::class);
}
}

View File

@ -1,117 +0,0 @@
<?php
namespace App\Services\Helpers;
use Illuminate\Support\Arr;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Contracts\Filesystem\Filesystem;
use App\Exceptions\ManifestDoesNotExistException;
class AssetHashService
{
/**
* Location of the manifest file generated by gulp.
*/
public const MANIFEST_PATH = './assets/manifest.json';
private Filesystem $filesystem;
protected static mixed $manifest = null;
/**
* AssetHashService constructor.
*/
public function __construct(FilesystemManager $filesystem)
{
$this->filesystem = $filesystem->createLocalDriver(['root' => public_path()]);
}
/**
* Modify a URL to append the asset hash.
*/
public function url(string $resource): string
{
$file = last(explode('/', $resource));
$data = Arr::get($this->manifest(), $file) ?? $file;
return str_replace($file, Arr::get($data, 'src') ?? $file, $resource);
}
/**
* Return the data integrity hash for a resource.
*/
public function integrity(string $resource): string
{
$file = last(explode('/', $resource));
$data = array_get($this->manifest(), $file, $file);
return Arr::get($data, 'integrity') ?? '';
}
/**
* Return a built CSS import using the provided URL.
*/
public function css(string $resource): string
{
$attributes = [
'href' => $this->url($resource),
'rel' => 'stylesheet preload',
'as' => 'style',
'crossorigin' => 'anonymous',
'referrerpolicy' => 'no-referrer',
];
if (config('panel.assets.use_hash')) {
$attributes['integrity'] = $this->integrity($resource);
}
$output = '<link';
foreach ($attributes as $key => $value) {
$output .= " $key=\"$value\"";
}
return $output . '>';
}
/**
* Return a built JS import using the provided URL.
*/
public function js(string $resource): string
{
$attributes = [
'src' => $this->url($resource),
'crossorigin' => 'anonymous',
];
if (config('panel.assets.use_hash')) {
$attributes['integrity'] = $this->integrity($resource);
}
$output = '<script';
foreach ($attributes as $key => $value) {
$output .= " $key=\"$value\"";
}
return $output . '></script>';
}
/**
* Get the asset manifest and store it in the cache for quicker lookups.
*/
protected function manifest(): array
{
if (static::$manifest === null) {
self::$manifest = json_decode(
$this->filesystem->get(self::MANIFEST_PATH),
true
);
}
$manifest = static::$manifest;
if ($manifest === null) {
throw new ManifestDoesNotExistException();
}
return $manifest;
}
}

View File

@ -1,34 +0,0 @@
module.exports = function (api) {
let targets = {};
const plugins = [
'babel-plugin-macros',
'styled-components',
'react-hot-loader/babel',
'@babel/transform-runtime',
'@babel/transform-react-jsx',
'@babel/proposal-class-properties',
'@babel/proposal-object-rest-spread',
'@babel/proposal-optional-chaining',
'@babel/proposal-nullish-coalescing-operator',
'@babel/syntax-dynamic-import',
];
if (api.env('test')) {
targets = { node: 'current' };
plugins.push('@babel/transform-modules-commonjs');
}
return {
plugins,
presets: [
'@babel/typescript',
['@babel/env', {
modules: false,
useBuiltIns: 'entry',
corejs: 3,
targets,
}],
'@babel/react',
]
};
};

View File

@ -10,7 +10,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->redirectGuestsTo(fn () => route('auth.login'));
$middleware->redirectGuestsTo(fn () => route('filament.app.auth.login'));
$middleware->web(\App\Http\Middleware\LanguageMiddleware::class);

View File

@ -9,6 +9,5 @@ return [
App\Providers\Filament\AppPanelProvider::class,
App\Providers\Filament\ServerPanelProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\ViewComposerServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,
];

View File

@ -3,6 +3,7 @@
return [
'name' => env('APP_NAME', 'Pelican'),
'logo' => env('APP_LOGO', '/pelican.svg'),
'favicon' => env('APP_FAVICON', '/pelican.ico'),
'version' => 'canary',

View File

@ -1,28 +0,0 @@
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig');
/** @type {import('ts-jest').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
globals: {
'ts-jest': {
isolatedModules: true,
},
},
moduleFileExtensions: ['js', 'ts', 'tsx', 'd.ts', 'json', 'node'],
moduleNameMapper: {
'\\.(jpe?g|png|gif|svg)$': '<rootDir>/resources/scripts/__mocks__/file.ts',
'\\.(s?css|less)$': 'identity-obj-proxy',
...pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
}),
},
setupFilesAfterEnv: [
'<rootDir>/resources/scripts/setup-tests.ts',
],
transform: {
'.*\\.[t|j]sx$': 'babel-jest',
'.*\\.ts$': 'ts-jest',
},
testPathIgnorePatterns: ['/node_modules/'],
};

View File

@ -1,158 +1,21 @@
{
"name": "panel",
"engines": {
"node": ">=18"
},
"dependencies": {
"@floating-ui/react-dom-interactions": "^0.6.6",
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.11",
"@headlessui/react": "^1.6.4",
"@heroicons/react": "^1.0.6",
"@hot-loader/react-dom": "^16.14.0",
"@preact/signals-react": "^1.2.1",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/line-clamp": "^0.4.0",
"axios": "^1.6.7",
"boring-avatars": "^1.7.0",
"chart.js": "^3.8.0",
"classnames": "^2.3.1",
"codemirror": "^5.58.2",
"copy-to-clipboard": "^3.3.1",
"date-fns": "^2.28.0",
"debounce": "^1.2.0",
"deepmerge-ts": "^4.2.1",
"easy-peasy": "^4.0.1",
"events": "^3.0.0",
"formik": "^2.2.6",
"framer-motion": "^6.3.10",
"i18next": "^21.8.9",
"i18next-http-backend": "^1.4.1",
"i18next-multiload-backend-adapter": "^1.0.0",
"qrcode.react": "^1.0.1",
"react": "^16.14.0",
"react-chartjs-2": "^4.2.0",
"react-dom": "npm:@hot-loader/react-dom",
"react-fast-compare": "^3.2.0",
"react-hot-loader": "^4.12.21",
"react-i18next": "^11.2.1",
"react-router-dom": "^5.1.2",
"react-transition-group": "^4.4.1",
"react-turnstile": "^1.1.4",
"rimraf": "^4",
"sockette": "^2.0.6",
"styled-components": "^5.2.1",
"styled-components-breakpoint": "^3.0.0-preview.20",
"swr": "^0.2.3",
"tailwindcss": "^3.0.24",
"use-fit-text": "^2.4.0",
"uuid": "^8.3.2",
"xterm": "^4.19.0",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-search": "^0.9.0",
"xterm-addon-search-bar": "^0.2.0",
"xterm-addon-web-links": "^0.6.0",
"yup": "^0.29.1"
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@babel/core": "^7.12.1",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.18.2",
"@babel/plugin-transform-react-jsx": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/preset-react": "^7.12.1",
"@babel/preset-typescript": "^7.12.1",
"@babel/runtime": "^7.12.1",
"@testing-library/dom": "^8.14.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "^14.2.1",
"@types/codemirror": "^0.0.98",
"@types/debounce": "^1.2.0",
"@types/events": "^3.0.0",
"@types/jest": "^28.1.3",
"@types/node": "^14.11.10",
"@types/qrcode.react": "^1.0.1",
"@types/react": "^16.14.0",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "^16.9.16",
"@types/react-redux": "^7.1.1",
"@types/react-router": "^5.1.3",
"@types/react-router-dom": "^5.1.3",
"@types/react-transition-group": "^4.4.0",
"@types/styled-components": "^5.1.7",
"@types/uuid": "^3.4.5",
"@types/webpack-env": "^1.15.2",
"@types/yup": "^0.29.3",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"autoprefixer": "^10.4.7",
"babel-jest": "^28.1.1",
"babel-loader": "^8.2.5",
"babel-plugin-styled-components": "^2.0.7",
"cross-env": "^7.0.2",
"css-loader": "^5.2.7",
"eslint": "^8.18.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"fork-ts-checker-webpack-plugin": "^6.2.10",
"identity-obj-proxy": "^3.0.0",
"jest": "^28.1.1",
"postcss": "^8.4.35",
"postcss-import": "^14.1.0",
"postcss-loader": "^4.0.0",
"postcss-nesting": "^10.1.8",
"postcss-preset-env": "^7.7.1",
"prettier": "^2.7.1",
"redux-devtools-extension": "^2.13.8",
"source-map-loader": "^1.1.3",
"style-loader": "^2.0.0",
"svg-url-loader": "^7.1.1",
"terser-webpack-plugin": "^4.2.3",
"ts-essentials": "^9.1.2",
"ts-jest": "^28.0.5",
"twin.macro": "^2.8.2",
"typescript": "^4.7.3",
"webpack": "^4.47.0",
"webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"yarn-deduplicate": "^1.1.1"
},
"scripts": {
"clean": "cd public/assets && rimraf -g *.js *.map",
"test": "jest",
"lint": "eslint ./resources/scripts/**/*.{ts,tsx} --ext .ts,.tsx",
"watch": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
"build": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --progress",
"build:production": "yarn run clean && cross-env NODE_ENV=production ./node_modules/.bin/webpack --mode production",
"serve": "yarn run clean && cross-env WEBPACK_PUBLIC_PATH=/webpack@hmr/ NODE_ENV=development webpack-dev-server --host 0.0.0.0 --port 8080 --public https://panel.test --hot"
},
"browserslist": [
"> 0.5%",
"last 2 versions",
"firefox esr",
"not dead"
],
"babelMacros": {
"twin": {
"preset": "styled-components"
},
"styledComponents": {
"pure": true,
"displayName": true,
"fileName": true
}
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.20",
"axios": "^1.7.4",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^1.0",
"postcss": "^8.4.47",
"postcss-nesting": "^13.0.1",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.13",
"vite": "^6.0"
}
}

View File

@ -1,17 +1,7 @@
module.exports = {
plugins: [
require('postcss-import'),
// We want to make use of nesting following the CSS Nesting spec, and not the
// SASS style nesting.
//
// @see https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting
require('tailwindcss/nesting')(require('postcss-nesting')),
require('tailwindcss'),
require('autoprefixer'),
require('postcss-preset-env')({
features: {
'nesting-rules': false,
},
}),
],
export default {
plugins: {
'tailwindcss/nesting': 'postcss-nesting',
tailwindcss: {},
autoprefixer: {},
},
};

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,3 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;

1
resources/js/app.js Normal file
View File

@ -0,0 +1 @@
import './bootstrap';

4
resources/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,4 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@ -1,30 +0,0 @@
import React from 'react';
import { Route } from 'react-router';
import { SwitchTransition } from 'react-transition-group';
import Fade from '@/components/elements/Fade';
import styled from 'styled-components/macro';
import tw from 'twin.macro';
const StyledSwitchTransition = styled(SwitchTransition)`
${tw`relative`};
& section {
${tw`absolute w-full top-0 left-0`};
}
`;
const TransitionRouter: React.FC = ({ children }) => {
return (
<Route
render={({ location }) => (
<StyledSwitchTransition>
<Fade timeout={150} key={location.pathname + location.search} in appear unmountOnExit>
<section>{children}</section>
</Fade>
</StyledSwitchTransition>
)}
/>
);
};
export default TransitionRouter;

View File

@ -1 +0,0 @@
module.exports = 'test-file-stub';

View File

@ -1,33 +0,0 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr';
import { ActivityLog, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { toPaginatedSet } from '@definitions/helpers';
import useFilteredObject from '@/plugins/useFilteredObject';
import { useUserSWRKey } from '@/plugins/useSWRKey';
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
const useActivityLogs = (
filters?: ActivityLogFilters,
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
const key = useUserSWRKey(['account', 'activity', JSON.stringify(useFilteredObject(filters || {}))]);
return useSWR<PaginatedResult<ActivityLog>>(
key,
async () => {
const { data } = await http.get('/api/client/account/activity', {
params: {
...withQueryBuilderParams(filters),
include: ['actor'],
},
});
return toPaginatedSet(data, Transformers.toActivityLog);
},
{ revalidateOnMount: false, ...(config || {}) }
);
};
export { useActivityLogs };

View File

@ -1,19 +0,0 @@
import http from '@/api/http';
import { ApiKey, rawDataToApiKey } from '@/api/account/getApiKeys';
export default (description: string, allowedIps: string): Promise<ApiKey & { secretToken: string }> => {
return new Promise((resolve, reject) => {
http.post('/api/client/account/api-keys', {
description,
allowed_ips: allowedIps.length > 0 ? allowedIps.split('\n') : [],
})
.then(({ data }) =>
resolve({
...rawDataToApiKey(data.attributes),
// eslint-disable-next-line camelcase
secretToken: data.meta?.secret_token ?? '',
})
)
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (identifier: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/client/account/api-keys/${identifier}`)
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (password: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete('/api/client/account/two-factor', { params: { password } })
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,7 +0,0 @@
import http from '@/api/http';
export default async (code: string, password: string): Promise<string[]> => {
const { data } = await http.post('/api/client/account/two-factor', { code, password });
return data.attributes.tokens;
};

View File

@ -1,25 +0,0 @@
import http from '@/api/http';
export interface ApiKey {
identifier: string;
description: string;
allowedIps: string[];
createdAt: Date | null;
lastUsedAt: Date | null;
}
export const rawDataToApiKey = (data: any): ApiKey => ({
identifier: data.identifier,
description: data.description,
allowedIps: data.allowed_ips,
createdAt: data.created_at ? new Date(data.created_at) : null,
lastUsedAt: data.last_used_at ? new Date(data.last_used_at) : null,
});
export default (): Promise<ApiKey[]> => {
return new Promise((resolve, reject) => {
http.get('/api/client/account/api-keys')
.then(({ data }) => resolve((data.data || []).map((d: any) => rawDataToApiKey(d.attributes))))
.catch(reject);
});
};

View File

@ -1,38 +0,0 @@
/* The MIT License (MIT)
Pterodactyl®
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
import http from '@/api/http';
export interface TwoFactorTokenData {
// eslint-disable-next-line camelcase
image_url_data: string;
secret: string;
}
export default (): Promise<TwoFactorTokenData> => {
return new Promise((resolve, reject) => {
http.get('/api/client/account/two-factor')
.then(({ data }) => resolve(data.data))
.catch(reject);
});
};

View File

@ -1,32 +0,0 @@
import useSWR, { ConfigInterface } from 'swr';
import http, { FractalResponseList } from '@/api/http';
import { SSHKey, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import { useUserSWRKey } from '@/plugins/useSWRKey';
const useSSHKeys = (config?: ConfigInterface<SSHKey[], AxiosError>) => {
const key = useUserSWRKey(['account', 'ssh-keys']);
return useSWR(
key,
async () => {
const { data } = await http.get('/api/client/account/ssh-keys');
return (data as FractalResponseList).data.map((datum: any) => {
return Transformers.toSSHKey(datum.attributes);
});
},
{ revalidateOnMount: false, ...(config || {}) }
);
};
const createSSHKey = async (name: string, publicKey: string): Promise<SSHKey> => {
const { data } = await http.post('/api/client/account/ssh-keys', { name, public_key: publicKey });
return Transformers.toSSHKey(data.attributes);
};
const deleteSSHKey = async (fingerprint: string): Promise<void> =>
await http.post('/api/client/account/ssh-keys/remove', { fingerprint });
export { useSSHKeys, createSSHKey, deleteSSHKey };

View File

@ -1,9 +0,0 @@
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);
});
};

View File

@ -1,19 +0,0 @@
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', {
current_password: current,
password: password,
password_confirmation: confirmPassword,
})
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,38 +0,0 @@
import http from '@/api/http';
export interface LoginResponse {
complete: boolean;
intended?: string;
confirmationToken?: string;
}
export interface LoginData {
username: string;
password: string;
recaptchaData?: string | null;
}
export default ({ username, password, recaptchaData }: LoginData): Promise<LoginResponse> => {
return new Promise((resolve, reject) => {
http.get('/sanctum/csrf-cookie')
.then(() =>
http.post('/auth/login', {
user: username,
password,
'cf-turnstile-response': recaptchaData,
})
)
.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);
});
};

View File

@ -1,19 +0,0 @@
import http from '@/api/http';
import { LoginResponse } from '@/api/auth/login';
export default (token: string, code: string, recoveryToken?: string): Promise<LoginResponse> => {
return new Promise((resolve, reject) => {
http.post('/auth/login/checkpoint', {
confirmation_token: token,
authentication_code: code,
recovery_token: recoveryToken && recoveryToken.length > 0 ? recoveryToken : undefined,
})
.then((response) =>
resolve({
complete: response.data.data.complete,
intended: response.data.data.intended || undefined,
})
)
.catch(reject);
});
};

View File

@ -1,30 +0,0 @@
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,
password_confirmation: data.passwordConfirmation,
})
.then((response) =>
resolve({
redirectTo: response.data.redirect_to,
sendToLogin: response.data.send_to_login,
})
)
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (email: string, recaptchaData?: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.post('/auth/password', { email, 'cf-turnstile-response': recaptchaData })
.then((response) => resolve(response.data.status || ''))
.catch(reject);
});
};

View File

@ -1,55 +0,0 @@
import {
FractalPaginatedResponse,
FractalResponseData,
FractalResponseList,
getPaginationSet,
PaginatedResult,
} from '@/api/http';
import { Model } from '@definitions/index';
type TransformerFunc<T> = (callback: FractalResponseData) => T;
const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';
function transform<T, M>(data: null | undefined, transformer: TransformerFunc<T>, missing?: M): M;
function transform<T, M>(
data: FractalResponseData | null | undefined,
transformer: TransformerFunc<T>,
missing?: M
): T | M;
function transform<T, M>(
data: FractalResponseList | FractalPaginatedResponse | null | undefined,
transformer: TransformerFunc<T>,
missing?: M
): T[] | M;
function transform<T>(
data: FractalResponseData | FractalResponseList | FractalPaginatedResponse | null | undefined,
transformer: TransformerFunc<T>,
missing = undefined
) {
if (data === undefined || data === null) {
return missing;
}
if (isList(data)) {
return data.data.map(transformer);
}
if (!data || !data.attributes || data.object === 'null_resource') {
return missing;
}
return transformer(data);
}
function toPaginatedSet<T extends TransformerFunc<Model>>(
response: FractalPaginatedResponse,
transformer: T
): PaginatedResult<ReturnType<T>> {
return {
items: transform(response, transformer) as ReturnType<T>[],
pagination: getPaginationSet(response.meta.pagination),
};
}
export { transform, toPaginatedSet };

View File

@ -1,33 +0,0 @@
import { MarkRequired } from 'ts-essentials';
import { FractalResponseData, FractalResponseList } from '../http';
export type UUID = string;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Model {}
interface ModelWithRelationships extends Model {
relationships: Record<string, FractalResponseData | FractalResponseList | undefined>;
}
/**
* Allows a model to have optional relationships that are marked as being
* present in a given pathway. This allows different API calls to specify the
* "completeness" of a response object without having to make every API return
* the same information, or every piece of logic do explicit null checking.
*
* Example:
* >> const user: WithLoaded<User, 'servers'> = {};
* >> // "user.servers" is no longer potentially undefined.
*/
type WithLoaded<M extends ModelWithRelationships, R extends keyof M['relationships']> = M & {
relationships: MarkRequired<M['relationships'], R>;
};
/**
* Helper type that allows you to infer the type of an object by giving
* it the specific API request function with a return type. For example:
*
* type Egg = InferModel<typeof getEgg>;
*/
export type InferModel<T extends (...args: any) => any> = ReturnType<T> extends Promise<infer U> ? U : T;

View File

@ -1,2 +0,0 @@
export * from './models.d';
export { default as Transformers, MetaTransformers } from './transformers';

View File

@ -1,35 +0,0 @@
import { Model, UUID } from '@/api/definitions';
import { SubuserPermission } from '@/state/server/subusers';
interface User extends Model {
uuid: string;
username: string;
email: string;
image: string;
twoFactorEnabled: boolean;
createdAt: Date;
permissions: SubuserPermission[];
can(permission: SubuserPermission): boolean;
}
interface SSHKey extends Model {
name: string;
publicKey: string;
fingerprint: string;
createdAt: Date;
}
interface ActivityLog extends Model<'actor'> {
id: string;
batch: UUID | null;
event: string;
ip: string | null;
isApi: boolean;
description: string | null;
properties: Record<string, string | unknown>;
hasAdditionalMetadata: boolean;
timestamp: Date;
relationships: {
actor: User | null;
};
}

View File

@ -1,50 +0,0 @@
import * as Models from '@definitions/user/models';
import { FractalResponseData } from '@/api/http';
import { transform } from '@definitions/helpers';
export default class Transformers {
static toSSHKey = (data: Record<any, any>): Models.SSHKey => {
return {
name: data.name,
publicKey: data.public_key,
fingerprint: data.fingerprint,
createdAt: new Date(data.created_at),
};
};
static toUser = ({ attributes }: FractalResponseData): Models.User => {
return {
uuid: attributes.uuid,
username: attributes.username,
email: attributes.email,
image: attributes.image,
twoFactorEnabled: attributes['2fa_enabled'],
permissions: attributes.permissions || [],
createdAt: new Date(attributes.created_at),
can(permission): boolean {
return this.permissions.includes(permission);
},
};
};
static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => {
const { actor } = attributes.relationships || {};
return {
id: attributes.id,
batch: attributes.batch,
event: attributes.event,
ip: attributes.ip,
isApi: attributes.is_api,
description: attributes.description,
properties: attributes.properties,
hasAdditionalMetadata: attributes.has_additional_metadata ?? false,
timestamp: new Date(attributes.timestamp),
relationships: {
actor: transform(actor as FractalResponseData, this.toUser, null),
},
};
};
}
export class MetaTransformers {}

View File

@ -1,26 +0,0 @@
import { rawDataToServerObject, Server } from '@/api/server/getServer';
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
interface QueryParams {
query?: string;
page?: number;
type?: string;
}
export default ({ query, ...params }: QueryParams): Promise<PaginatedResult<Server>> => {
return new Promise((resolve, reject) => {
http.get('/api/client', {
params: {
'filter[*]': query,
...params,
},
})
.then(({ data }) =>
resolve({
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum)),
pagination: getPaginationSet(data.meta.pagination),
})
)
.catch(reject);
});
};

View File

@ -1,10 +0,0 @@
import { PanelPermissions } from '@/state/permissions';
import http from '@/api/http';
export default (): Promise<PanelPermissions> => {
return new Promise((resolve, reject) => {
http.get('/api/client/permissions')
.then(({ data }) => resolve(data.attributes.permissions))
.catch(reject);
});
};

View File

@ -1,160 +0,0 @@
import axios, { AxiosInstance } from 'axios';
import { store } from '@/state';
const http: AxiosInstance = axios.create({
withCredentials: true,
timeout: 20000,
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
http.interceptors.request.use((req) => {
if (!req.url?.endsWith('/resources')) {
store.getActions().progress.startContinuous();
}
return req;
});
http.interceptors.response.use(
(resp) => {
if (!resp.request?.url?.endsWith('/resources')) {
store.getActions().progress.setComplete();
}
return resp;
},
(error) => {
store.getActions().progress.setComplete();
throw error;
}
);
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) {
let { data } = error.response;
// Some non-JSON requests can still return the error as a JSON block. In those cases, attempt
// to parse it into JSON so we can display an actual error.
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) {
// do nothing, bad json
}
}
if (data.errors && data.errors[0] && data.errors[0].detail) {
return data.errors[0].detail;
}
// Errors from daemon directory, mostly just for file uploads.
if (data.error && typeof data.error === 'string') {
return data.error;
}
}
return error.message;
}
export interface FractalResponseData {
object: string;
attributes: {
[k: string]: any;
relationships?: Record<string, FractalResponseData | FractalResponseList | null | undefined>;
};
}
export interface FractalResponseList {
object: 'list';
data: FractalResponseData[];
}
export interface FractalPaginatedResponse extends FractalResponseList {
meta: {
pagination: {
total: number;
count: number;
/* eslint-disable camelcase */
per_page: number;
current_page: number;
total_pages: number;
/* eslint-enable camelcase */
};
};
}
export interface PaginatedResult<T> {
items: T[];
pagination: PaginationDataSet;
}
export interface PaginationDataSet {
total: number;
count: number;
perPage: number;
currentPage: number;
totalPages: number;
}
export function getPaginationSet(data: any): PaginationDataSet {
return {
total: data.total,
count: data.count,
perPage: data.per_page,
currentPage: data.current_page,
totalPages: data.total_pages,
};
}
type QueryBuilderFilterValue = string | number | boolean | null;
export interface QueryBuilderParams<FilterKeys extends string = string, SortKeys extends string = string> {
page?: number;
filters?: {
[K in FilterKeys]?: QueryBuilderFilterValue | Readonly<QueryBuilderFilterValue[]>;
};
sorts?: {
[K in SortKeys]?: -1 | 0 | 1 | 'asc' | 'desc' | null;
};
}
/**
* Helper function that parses a data object provided and builds query parameters
* for the Laravel Query Builder package automatically. This will apply sorts and
* filters deterministically based on the provided values.
*/
export const withQueryBuilderParams = (data?: QueryBuilderParams): Record<string, unknown> => {
if (!data) return {};
const filters = Object.keys(data.filters || {}).reduce((obj, key) => {
const value = data.filters?.[key];
return !value || value === '' ? obj : { ...obj, [`filter[${key}]`]: value };
}, {} as NonNullable<QueryBuilderParams['filters']>);
const sorts = Object.keys(data.sorts || {}).reduce((arr, key) => {
const value = data.sorts?.[key];
if (!value || !['asc', 'desc', 1, -1].includes(value)) {
return arr;
}
return [...arr, (value === -1 || value === 'desc' ? '-' : '') + key];
}, [] as string[]);
return {
...filters,
sort: !sorts.length ? undefined : sorts.join(','),
page: data.page,
};
};

View File

@ -1,21 +0,0 @@
import http from '@/api/http';
import { AxiosError } from 'axios';
import { History } from 'history';
export const setupInterceptors = (history: History) => {
http.interceptors.response.use(
(resp) => resp,
(error: AxiosError) => {
if (error.response?.status === 400) {
if (
(error.response?.data as Record<string, any>).errors?.[0].code === 'TwoFactorAuthRequiredException'
) {
if (!window.location.pathname.startsWith('/account')) {
history.replace('/account', { twoFactorRedirect: true });
}
}
}
throw error;
}
);
};

View File

@ -1,35 +0,0 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr';
import { ActivityLog, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { toPaginatedSet } from '@definitions/helpers';
import useFilteredObject from '@/plugins/useFilteredObject';
import { useServerSWRKey } from '@/plugins/useSWRKey';
import { ServerContext } from '@/state/server';
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
const useActivityLogs = (
filters?: ActivityLogFilters,
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
const key = useServerSWRKey(['activity', useFilteredObject(filters || {})]);
return useSWR<PaginatedResult<ActivityLog>>(
key,
async () => {
const { data } = await http.get(`/api/client/servers/${uuid}/activity`, {
params: {
...withQueryBuilderParams(filters),
include: ['actor'],
},
});
return toPaginatedSet(data, Transformers.toActivityLog);
},
{ revalidateOnMount: false, ...(config || {}) }
);
};
export { useActivityLogs };

View File

@ -1,19 +0,0 @@
import http from '@/api/http';
import { ServerBackup } from '@/api/server/types';
import { rawDataToServerBackup } from '@/api/transformers';
interface RequestParameters {
name?: string;
ignored?: string;
isLocked: boolean;
}
export default async (uuid: string, params: RequestParameters): Promise<ServerBackup> => {
const { data } = await http.post(`/api/client/servers/${uuid}/backups`, {
name: params.name,
ignored: params.ignored,
is_locked: params.isLocked,
});
return rawDataToServerBackup(data);
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, backup: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/client/servers/${uuid}/backups/${backup}`)
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, backup: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${uuid}/backups/${backup}/download`)
.then(({ data }) => resolve(data.attributes.url))
.catch(reject);
});
};

View File

@ -1,7 +0,0 @@
import http from '@/api/http';
export const restoreServerBackup = async (uuid: string, backup: string, truncate?: boolean): Promise<void> => {
await http.post(`/api/client/servers/${uuid}/backups/${backup}/restore`, {
truncate,
});
};

View File

@ -1,19 +0,0 @@
import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/databases/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);
});
};

View File

@ -1,9 +0,0 @@
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);
});
};

View File

@ -1,31 +0,0 @@
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.password?.attributes?.password,
});
export default (uuid: string, includePassword = true): Promise<ServerDatabase[]> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${uuid}/databases`, {
params: includePassword ? { include: 'password' } : undefined,
})
.then((response) =>
resolve((response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes)))
)
.catch(reject);
});
};

View File

@ -1,10 +0,0 @@
import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/databases/getServerDatabases';
import http from '@/api/http';
export default (uuid: string, database: string): Promise<ServerDatabase> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/databases/${database}/rotate-password`)
.then((response) => resolve(rawDataToServerDatabase(response.data.attributes)))
.catch(reject);
});
};

View File

@ -1,37 +0,0 @@
/* The MIT License (MIT)
Pterodactyl®
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
import http from '@/api/http';
interface Data {
file: string;
mode: string;
}
export default (uuid: string, directory: string, files: Data[]): Promise<void> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/files/chmod`, { root: directory, files })
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,17 +0,0 @@
import { FileObject } from '@/api/server/files/loadDirectory';
import http from '@/api/http';
import { rawDataToFileObject } from '@/api/transformers';
export default async (uuid: string, directory: string, files: string[]): Promise<FileObject> => {
const { data } = await http.post(
`/api/client/servers/${uuid}/files/compress`,
{ root: directory, files },
{
timeout: 60000,
timeoutErrorMessage:
'It looks like this archive is taking a long time to generate. It will appear once completed.',
}
);
return rawDataToFileObject(data);
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, location: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/files/copy`, { location })
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, root: string, name: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/files/create-folder`, { root, name })
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,13 +0,0 @@
import http from '@/api/http';
export default async (uuid: string, directory: string, file: string): Promise<void> => {
await http.post(
`/api/client/servers/${uuid}/files/decompress`,
{ root: directory, file },
{
timeout: 300000,
timeoutErrorMessage:
'It looks like this archive is taking a long time to be unarchived. Once completed the unarchived files will appear.',
}
);
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, directory: string, files: string[]): Promise<void> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/files/delete`, { root: directory, files })
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,13 +0,0 @@
import http from '@/api/http';
export default (server: string, file: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/files/contents`, {
params: { file },
transformResponse: (res) => res,
responseType: 'text',
})
.then(({ data }) => resolve(data))
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, file: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${uuid}/files/download`, { params: { file } })
.then(({ data }) => resolve(data.attributes.url))
.catch(reject);
});
};

View File

@ -1,32 +0,0 @@
/* The MIT License (MIT)
Pterodactyl®
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
import http from '@/api/http';
export default (uuid: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${uuid}/files/upload`)
.then(({ data }) => resolve(data.attributes.url))
.catch(reject);
});
};

View File

@ -1,25 +0,0 @@
import http from '@/api/http';
import { rawDataToFileObject } from '@/api/transformers';
export interface FileObject {
key: string;
name: string;
mode: string;
modeBits: string;
size: number;
isFile: boolean;
isSymlink: boolean;
mimetype: string;
createdAt: Date;
modifiedAt: Date;
isArchiveType: () => boolean;
isEditable: () => boolean;
}
export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
params: { directory: directory ?? '/' },
});
return (data.data || []).map(rawDataToFileObject);
};

View File

@ -1,14 +0,0 @@
import http from '@/api/http';
interface Data {
to: string;
from: string;
}
export default (uuid: string, directory: string, files: Data[]): Promise<void> => {
return new Promise((resolve, reject) => {
http.put(`/api/client/servers/${uuid}/files/rename`, { root: directory, files })
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,10 +0,0 @@
import http from '@/api/http';
export default async (uuid: string, file: string, content: string): Promise<void> => {
await http.post(`/api/client/servers/${uuid}/files/write`, content, {
params: { file },
headers: {
'Content-Type': 'text/plain',
},
});
};

View File

@ -1,89 +0,0 @@
import http, { FractalResponseData, FractalResponseList } from '@/api/http';
import { rawDataToServerAllocation, rawDataToServerEggVariable } from '@/api/transformers';
import { ServerEggVariable, ServerStatus } from '@/api/server/types';
export interface Allocation {
id: number;
ip: string;
alias: string | null;
port: number;
notes: string | null;
isDefault: boolean;
}
export interface Server {
id: string;
internalId: number | string;
uuid: string;
name: string;
node: string;
isNodeUnderMaintenance: boolean;
status: ServerStatus;
sftpDetails: {
ip: string;
alias: string;
port: number;
};
invocation: string;
dockerImage: string;
description: string;
limits: {
memory: number;
swap: number;
disk: number;
io: number;
cpu: number;
threads: string;
};
eggFeatures: string[];
featureLimits: {
databases: number;
allocations: number;
backups: number;
};
isTransferring: boolean;
variables: ServerEggVariable[];
allocations: Allocation[];
}
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
id: data.identifier,
internalId: data.internal_id,
uuid: data.uuid,
name: data.name,
node: data.node,
isNodeUnderMaintenance: data.is_node_under_maintenance,
status: data.status,
invocation: data.invocation,
dockerImage: data.docker_image,
sftpDetails: {
ip: data.sftp_details.ip,
alias: data.sftp_details.alias,
port: data.sftp_details.port,
},
description: data.description ? (data.description.length > 0 ? data.description : null) : null,
limits: { ...data.limits },
eggFeatures: data.egg_features || [],
featureLimits: { ...data.feature_limits },
isTransferring: data.is_transferring,
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(
rawDataToServerEggVariable
),
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(
rawDataToServerAllocation
),
});
export default (uuid: string): Promise<[Server, string[]]> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${uuid}`)
.then(({ data }) =>
resolve([
rawDataToServerObject(data),
// eslint-disable-next-line camelcase
data.meta?.is_server_owner ? ['*'] : data.meta?.user_permissions || [],
])
)
.catch(reject);
});
};

View File

@ -1,33 +0,0 @@
import http from '@/api/http';
export type ServerPowerState = 'offline' | 'starting' | 'running' | 'stopping';
export interface ServerStats {
status: ServerPowerState;
isSuspended: boolean;
memoryUsageInBytes: number;
cpuUsagePercent: number;
diskUsageInBytes: number;
networkRxInBytes: number;
networkTxInBytes: number;
uptime: number;
}
export default (server: string): Promise<ServerStats> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/resources`)
.then(({ data: { attributes } }) =>
resolve({
status: attributes.current_state,
isSuspended: attributes.is_suspended,
memoryUsageInBytes: attributes.resources.memory_bytes,
cpuUsagePercent: attributes.resources.cpu_absolute,
diskUsageInBytes: attributes.resources.disk_bytes,
networkRxInBytes: attributes.resources.network_rx_bytes,
networkTxInBytes: attributes.resources.network_tx_bytes,
uptime: attributes.resources.uptime,
})
)
.catch(reject);
});
};

View File

@ -1,19 +0,0 @@
import http from '@/api/http';
interface Response {
token: string;
socket: string;
}
export default (server: string): Promise<Response> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/websocket`)
.then(({ data }) =>
resolve({
token: data.data.token,
socket: data.data.socket,
})
)
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import { Allocation } from '@/api/server/getServer';
import http from '@/api/http';
import { rawDataToServerAllocation } from '@/api/transformers';
export default async (uuid: string): Promise<Allocation> => {
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations`);
return rawDataToServerAllocation(data);
};

View File

@ -1,5 +0,0 @@
import { Allocation } from '@/api/server/getServer';
import http from '@/api/http';
export default async (uuid: string, id: number): Promise<Allocation> =>
await http.delete(`/api/client/servers/${uuid}/network/allocations/${id}`);

View File

@ -1,9 +0,0 @@
import { Allocation } from '@/api/server/getServer';
import http from '@/api/http';
import { rawDataToServerAllocation } from '@/api/transformers';
export default async (uuid: string, id: number): Promise<Allocation> => {
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}/primary`);
return rawDataToServerAllocation(data);
};

View File

@ -1,9 +0,0 @@
import { Allocation } from '@/api/server/getServer';
import http from '@/api/http';
import { rawDataToServerAllocation } from '@/api/transformers';
export default async (uuid: string, id: number, notes: string | null): Promise<Allocation> => {
const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}`, { notes });
return rawDataToServerAllocation(data);
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/settings/reinstall`)
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, name: string, description?: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/settings/rename`, { name, description })
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,19 +0,0 @@
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
import http from '@/api/http';
type Data = Pick<Schedule, 'cron' | 'name' | 'onlyWhenOnline' | 'isActive'> & { id?: number };
export default async (uuid: string, schedule: Data): Promise<Schedule> => {
const { data } = await http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, {
is_active: schedule.isActive,
only_when_online: schedule.onlyWhenOnline,
name: schedule.name,
minute: schedule.cron.minute,
hour: schedule.cron.hour,
day_of_month: schedule.cron.dayOfMonth,
month: schedule.cron.month,
day_of_week: schedule.cron.dayOfWeek,
});
return rawDataToServerSchedule(data.attributes);
};

View File

@ -1,23 +0,0 @@
import { rawDataToServerTask, Task } from '@/api/server/schedules/getServerSchedules';
import http from '@/api/http';
interface Data {
action: string;
payload: string;
timeOffset: string | number;
continueOnFailure: boolean;
}
export default async (uuid: string, schedule: number, task: number | undefined, data: Data): Promise<Task> => {
const { data: response } = await http.post(
`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`,
{
action: data.action,
payload: data.payload,
continue_on_failure: data.continueOnFailure,
time_offset: data.timeOffset,
}
);
return rawDataToServerTask(response.attributes);
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, schedule: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/client/servers/${uuid}/schedules/${schedule}`)
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, scheduleId: number, taskId: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/client/servers/${uuid}/schedules/${scheduleId}/tasks/${taskId}`)
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,14 +0,0 @@
import http from '@/api/http';
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
export default (uuid: string, schedule: number): Promise<Schedule> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, {
params: {
include: ['tasks'],
},
})
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
.catch(reject);
});
};

View File

@ -1,77 +0,0 @@
import http from '@/api/http';
export interface Schedule {
id: number;
name: string;
cron: {
dayOfWeek: string;
month: string;
dayOfMonth: string;
hour: string;
minute: string;
};
isActive: boolean;
isProcessing: boolean;
onlyWhenOnline: boolean;
lastRunAt: Date | null;
nextRunAt: Date | null;
createdAt: Date;
updatedAt: Date;
tasks: Task[];
}
export interface Task {
id: number;
sequenceId: number;
action: string;
payload: string;
timeOffset: number;
isQueued: boolean;
continueOnFailure: boolean;
createdAt: Date;
updatedAt: Date;
}
export const rawDataToServerTask = (data: any): Task => ({
id: data.id,
sequenceId: data.sequence_id,
action: data.action,
payload: data.payload,
timeOffset: data.time_offset,
isQueued: data.is_queued,
continueOnFailure: data.continue_on_failure,
createdAt: new Date(data.created_at),
updatedAt: new Date(data.updated_at),
});
export const rawDataToServerSchedule = (data: any): Schedule => ({
id: data.id,
name: data.name,
cron: {
dayOfWeek: data.cron.day_of_week,
month: data.cron.month,
dayOfMonth: data.cron.day_of_month,
hour: data.cron.hour,
minute: data.cron.minute,
},
isActive: data.is_active,
isProcessing: data.is_processing,
onlyWhenOnline: data.only_when_online,
lastRunAt: data.last_run_at ? new Date(data.last_run_at) : null,
nextRunAt: data.next_run_at ? new Date(data.next_run_at) : null,
createdAt: new Date(data.created_at),
updatedAt: new Date(data.updated_at),
tasks: (data.relationships?.tasks?.data || []).map((row: any) => rawDataToServerTask(row.attributes)),
});
export default async (uuid: string): Promise<Schedule[]> => {
const { data } = await http.get(`/api/client/servers/${uuid}/schedules`, {
params: {
include: ['tasks'],
},
});
return (data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes));
};

View File

@ -1,4 +0,0 @@
import http from '@/api/http';
export default async (server: string, schedule: number): Promise<void> =>
await http.post(`/api/client/servers/${server}/schedules/${schedule}/execute`);

View File

@ -1,5 +0,0 @@
import http from '@/api/http';
export default async (uuid: string, image: string): Promise<void> => {
await http.put(`/api/client/servers/${uuid}/settings/docker-image`, { docker_image: image });
};

View File

@ -1,29 +0,0 @@
export type ServerStatus =
| 'installing'
| 'install_failed'
| 'reinstall_failed'
| 'suspended'
| 'restoring_backup'
| null;
export interface ServerBackup {
uuid: string;
isSuccessful: boolean;
isLocked: boolean;
name: string;
ignoredFiles: string;
checksum: string;
bytes: number;
createdAt: Date;
completedAt: Date | null;
}
export interface ServerEggVariable {
name: string;
description: string;
envVariable: string;
defaultValue: string;
serverValue: string | null;
isEditable: boolean;
rules: string[];
}

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
import { ServerEggVariable } from '@/api/server/types';
import { rawDataToServerEggVariable } from '@/api/transformers';
export default async (uuid: string, key: string, value: string): Promise<[ServerEggVariable, string]> => {
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
return [rawDataToServerEggVariable(data), data.meta.startup_command];
};

View File

@ -1,18 +0,0 @@
import http from '@/api/http';
import { rawDataToServerSubuser } from '@/api/server/users/getServerSubusers';
import { Subuser } from '@/state/server/subusers';
interface Params {
email: string;
permissions: string[];
}
export default (uuid: string, params: Params, subuser?: Subuser): Promise<Subuser> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/users${subuser ? `/${subuser.uuid}` : ''}`, {
...params,
})
.then((data) => resolve(rawDataToServerSubuser(data.data)))
.catch(reject);
});
};

View File

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (uuid: string, userId: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/client/servers/${uuid}/users/${userId}`)
.then(() => resolve())
.catch(reject);
});
};

View File

@ -1,21 +0,0 @@
import http, { FractalResponseData } from '@/api/http';
import { Subuser } from '@/state/server/subusers';
export const rawDataToServerSubuser = (data: FractalResponseData): Subuser => ({
uuid: data.attributes.uuid,
username: data.attributes.username,
email: data.attributes.email,
image: data.attributes.image,
twoFactorEnabled: data.attributes['2fa_enabled'],
createdAt: new Date(data.attributes.created_at),
permissions: data.attributes.permissions || [],
can: (permission) => (data.attributes.permissions || []).indexOf(permission) >= 0,
});
export default (uuid: string): Promise<Subuser[]> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${uuid}/users`)
.then(({ data }) => resolve((data.data || []).map(rawDataToServerSubuser)))
.catch(reject);
});
};

View File

@ -1,19 +0,0 @@
import { ServerContext } from '@/state/server';
import useSWR from 'swr';
import http from '@/api/http';
import { rawDataToServerAllocation } from '@/api/transformers';
import { Allocation } from '@/api/server/getServer';
export default () => {
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
return useSWR<Allocation[]>(
['server:allocations', uuid],
async () => {
const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`);
return (data.data || []).map(rawDataToServerAllocation);
},
{ revalidateOnFocus: false, revalidateOnMount: false }
);
};

View File

@ -1,30 +0,0 @@
import useSWR from 'swr';
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
import { ServerBackup } from '@/api/server/types';
import { rawDataToServerBackup } from '@/api/transformers';
import { ServerContext } from '@/state/server';
import { createContext, useContext } from 'react';
interface ctx {
page: number;
setPage: (value: number | ((s: number) => number)) => void;
}
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
type BackupResponse = PaginatedResult<ServerBackup> & { backupCount: number };
export default () => {
const { page } = useContext(Context);
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
return useSWR<BackupResponse>(['server:backups', uuid, page], async () => {
const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } });
return {
items: (data.data || []).map(rawDataToServerBackup),
pagination: getPaginationSet(data.meta.pagination),
backupCount: data.meta.backup_count,
};
});
};

View File

@ -1,27 +0,0 @@
import useSWR, { ConfigInterface } from 'swr';
import http, { FractalResponseList } from '@/api/http';
import { rawDataToServerEggVariable } from '@/api/transformers';
import { ServerEggVariable } from '@/api/server/types';
interface Response {
invocation: string;
variables: ServerEggVariable[];
dockerImages: Record<string, string>;
}
export default (uuid: string, initialData?: Response | null, config?: ConfigInterface<Response>) =>
useSWR(
[uuid, '/startup'],
async (): Promise<Response> => {
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
return {
variables,
invocation: data.meta.startup_command,
dockerImages: data.meta.docker_images || {},
};
},
{ initialData: initialData || undefined, errorRetryCount: 3, ...(config || {}) }
);

View File

@ -1,77 +0,0 @@
import { Allocation } from '@/api/server/getServer';
import { FractalResponseData } from '@/api/http';
import { FileObject } from '@/api/server/files/loadDirectory';
import { ServerBackup, ServerEggVariable } from '@/api/server/types';
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
id: data.attributes.id,
ip: data.attributes.ip,
alias: data.attributes.ip_alias,
port: data.attributes.port,
notes: data.attributes.notes,
isDefault: data.attributes.is_default,
});
export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
key: `${data.attributes.is_file ? 'file' : 'dir'}_${data.attributes.name}`,
name: data.attributes.name,
mode: data.attributes.mode,
modeBits: data.attributes.mode_bits,
size: Number(data.attributes.size),
isFile: data.attributes.is_file,
isSymlink: data.attributes.is_symlink,
mimetype: data.attributes.mimetype,
createdAt: new Date(data.attributes.created_at),
modifiedAt: new Date(data.attributes.modified_at),
isArchiveType: function () {
return (
this.isFile &&
[
'application/vnd.rar', // .rar
'application/x-rar-compressed', // .rar (2)
'application/x-tar', // .tar
'application/x-br', // .tar.br
'application/x-bzip2', // .tar.bz2, .bz2
'application/gzip', // .tar.gz, .gz
'application/x-gzip',
'application/x-lzip', // .tar.lz4, .lz4 (not sure if this mime type is correct)
'application/x-sz', // .tar.sz, .sz (not sure if this mime type is correct)
'application/x-xz', // .tar.xz, .xz
'application/x-7z-compressed', // .7z
'application/zstd', // .tar.zst, .zst
'application/zip', // .zip
].indexOf(this.mimetype) >= 0
);
},
isEditable: function () {
if (this.isArchiveType() || !this.isFile) return false;
const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\/(?!svg\+xml)/];
return matches.every((m) => !this.mimetype.match(m));
},
});
export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({
uuid: attributes.uuid,
isSuccessful: attributes.is_successful,
isLocked: attributes.is_locked,
name: attributes.name,
ignoredFiles: attributes.ignored_files,
checksum: attributes.checksum,
bytes: attributes.bytes,
createdAt: new Date(attributes.created_at),
completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null,
});
export const rawDataToServerEggVariable = ({ attributes }: FractalResponseData): ServerEggVariable => ({
name: attributes.name,
description: attributes.description,
envVariable: attributes.env_variable,
defaultValue: attributes.default_value,
serverValue: attributes.server_value,
isEditable: attributes.is_editable,
rules: attributes.rules.split('|'),
});

Some files were not shown because too many files have changed in this diff Show More