From 38fe14837bbe194e03a3c50addf8ea6ff7455baa Mon Sep 17 00:00:00 2001 From: "Victor B." Date: Mon, 8 Apr 2024 13:17:29 +0200 Subject: [PATCH 1/4] feat: add "route eggs" setting to default extension config controller saved as a JSON array of egg IDs in `blueprint::extensionconfig_[id]_eggs`. ID of `-1` means the routes should be shown on all eggs --- .../ExtensionConfigurationController.php | 20 +++++++++++- .../BlueprintExtensionController.php | 2 +- .../private/build/extensions/controller.build | 2 ++ .../views/blueprint/admin/template.blade.php | 31 ++++++++++++++++++- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/BlueprintFramework/Controllers/ExtensionConfigurationController.php b/app/BlueprintFramework/Controllers/ExtensionConfigurationController.php index 76bd14c..d9e330b 100644 --- a/app/BlueprintFramework/Controllers/ExtensionConfigurationController.php +++ b/app/BlueprintFramework/Controllers/ExtensionConfigurationController.php @@ -20,7 +20,23 @@ class ExtensionConfigurationController extends Controller */ public function update(ExtensionConfigurationRequest $request): RedirectResponse { - foreach ($request->normalize() as $key => $value) { $this->settings->set('blueprint::extensionconfig_' . $key, $value); } + // set extension eggs to be -1 (Show all), then overwrite if needed + $this->settings->set('blueprint::extensionconfig_' . $request->input('_identifier', 'blueprint') . '_eggs', '["-1"]'); + + foreach ($request->normalize() as $key => $value) { + if (str_ends_with($key, '_eggs')) { + // if there are other eggs set, remove the -1 'egg' + $eggs = (array)$value['*']; + if (count($eggs) > 1 && in_array('-1', $eggs)) { + $eggs = array_diff($eggs, ['-1']); + } + + $value = json_encode(array_values($eggs)); + } + + $this->settings->set('blueprint::extensionconfig_' . $key, $value); + } + return redirect()->route('admin.extensions.'.$request->input('_identifier', 'blueprint').'.index'); } } @@ -31,6 +47,8 @@ class ExtensionConfigurationRequest extends AdminFormRequest return [ $this->input('_identifier', 'blueprint').'_adminlayouts' => 'boolean', $this->input('_identifier', 'blueprint').'_dashboardwrapper' => 'boolean', + $this->input('_identifier', 'blueprint').'_eggs' => 'array', + $this->input('_identifier', 'blueprint').'_eggs.*' => 'numeric', ]; } diff --git a/app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php b/app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php index e4ab3bb..97ccfba 100644 --- a/app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php +++ b/app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php @@ -2,9 +2,9 @@ namespace Pterodactyl\Http\Controllers\Admin\Extensions\Blueprint; +use Artisan; use Illuminate\View\View; use Illuminate\View\Factory as ViewFactory; -use Artisan; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\BlueprintFramework\Services\PlaceholderService\BlueprintPlaceholderService; use Pterodactyl\BlueprintFramework\Services\ConfigService\BlueprintConfigService; diff --git a/blueprint/extensions/blueprint/private/build/extensions/controller.build b/blueprint/extensions/blueprint/private/build/extensions/controller.build index 2b32eac..5937fe4 100644 --- a/blueprint/extensions/blueprint/private/build/extensions/controller.build +++ b/blueprint/extensions/blueprint/private/build/extensions/controller.build @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Controllers\Admin\Extensions\[id]; use Illuminate\View\View; +use Pterodactyl\Models\Egg; use Illuminate\View\Factory as ViewFactory; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Helpers\SoftwareVersionService; @@ -29,6 +30,7 @@ class [id]ExtensionController extends Controller return $this->view->make('admin.extensions.[id].index', [ 'blueprint' => $this->blueprint, 'version' => $this->version, + 'eggs' => Egg::all(), 'root' => $rootPath ]); } diff --git a/resources/views/blueprint/admin/template.blade.php b/resources/views/blueprint/admin/template.blade.php index 8d930b7..7f3f76f 100644 --- a/resources/views/blueprint/admin/template.blade.php +++ b/resources/views/blueprint/admin/template.blade.php @@ -55,6 +55,19 @@

Allow this extension to extend the dashboard's blade wrapper.

+ +
+
+ + +

Guess

+
+
-@endsection \ No newline at end of file +@endsection + +@section('footer-scripts') + @parent + + +@endsection From fe488495b0709da89a0dd315a664d11fdb5adb6e Mon Sep 17 00:00:00 2001 From: "Victor B." Date: Mon, 8 Apr 2024 13:55:15 +0200 Subject: [PATCH 2/4] feat `routes`: internal route for per-extension egg ID fetching --- .../Controllers/ExtensionRouteController.php | 30 +++++++++++++++++++ routes/blueprint/client.php | 9 +++++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 app/BlueprintFramework/Controllers/ExtensionRouteController.php diff --git a/app/BlueprintFramework/Controllers/ExtensionRouteController.php b/app/BlueprintFramework/Controllers/ExtensionRouteController.php new file mode 100644 index 0000000..4ed7a05 --- /dev/null +++ b/app/BlueprintFramework/Controllers/ExtensionRouteController.php @@ -0,0 +1,30 @@ +input('id', 'blueprint'); + $eggs = $this->settings->get('blueprint::extensionconfig_' . $id . '_eggs'); + return json_decode($eggs ?: '["-1"]'); + } +} + +class GetRouteEggsRequest extends ClientApiRequest { + public function authorize(): bool + { + return true; + } +} diff --git a/routes/blueprint/client.php b/routes/blueprint/client.php index e076d23..356a33a 100644 --- a/routes/blueprint/client.php +++ b/routes/blueprint/client.php @@ -1,9 +1,16 @@ getExtension() == 'php') { Route::prefix('/'.basename($partial->getFilename(), '.php')) ->group(function () use ($partial) {require_once $partial->getPathname();} ); } -} \ No newline at end of file +} + +/* Routes internally used by Blueprint. */ +Route::prefix('/blueprint')->group(function () { + Route::get('/eggs', [ExtensionRouteController::class, 'eggs']); +}); From 525e518e56f611e6a646f2168c6e15c2fe954e9b Mon Sep 17 00:00:00 2001 From: "Victor B." Date: Mon, 8 Apr 2024 13:57:22 +0200 Subject: [PATCH 3/4] add Pterodactyl's prettierrc with a tab width of 2 --- .prettierrc.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..fa31446 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "endOfLine": "lf" +} From 99bef29bb1900cfdfca93b5a2cbdeef93a8458c6 Mon Sep 17 00:00:00 2001 From: "Victor B." Date: Mon, 8 Apr 2024 14:26:01 +0200 Subject: [PATCH 4/4] feat `react`: add egg routes filtering --- resources/scripts/api/server/getServer.ts | 157 +++++++++--------- .../extends/routers/ServerRouter.tsx | 99 +++++++---- .../blueprint/extends/routers/routes.ts | 5 +- 3 files changed, 147 insertions(+), 114 deletions(-) diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts index 860c423..a19493b 100644 --- a/resources/scripts/api/server/getServer.ts +++ b/resources/scripts/api/server/getServer.ts @@ -3,91 +3,94 @@ import { rawDataToServerAllocation, rawDataToServerEggVariable } from '@/api/tra import { ServerEggVariable, ServerStatus } from '@/api/server/types'; export interface Allocation { - id: number; - ip: string; - alias: string | null; - port: number; - notes: string | null; - isDefault: boolean; + 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; - 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[]; - - BlueprintFramework: { - eggId: number; - }; + id: string; + internalId: number | string; + uuid: string; + name: string; + node: string; + isNodeUnderMaintenance: boolean; + status: ServerStatus; + sftpDetails: { + ip: 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[]; + + BlueprintFramework: { + eggId: number; + }; } 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, - 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 - ), + 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, + 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 + ), - BlueprintFramework: { ...data.BlueprintFramework } + BlueprintFramework: { + eggId: data.BlueprintFramework.egg_id, + }, }); 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); - }); + 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); + }); }; diff --git a/resources/scripts/blueprint/extends/routers/ServerRouter.tsx b/resources/scripts/blueprint/extends/routers/ServerRouter.tsx index 4114d16..d1d22ad 100644 --- a/resources/scripts/blueprint/extends/routers/ServerRouter.tsx +++ b/resources/scripts/blueprint/extends/routers/ServerRouter.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { NavLink, Route, Switch, useRouteMatch } from 'react-router-dom'; import TransitionRouter from '@/TransitionRouter'; import PermissionRoute from '@/components/elements/PermissionRoute'; @@ -12,6 +12,30 @@ import { ServerContext } from '@/state/server'; import routes from '@/routers/routes'; import blueprintRoutes from './routes'; +const blueprintExtensions = [...new Set(blueprintRoutes.server.map((route) => route.identifier))]; + +/** + * Get the route egg IDs for each extension with server routes. + */ +const useExtensionEggs = () => { + const [extensionEggs, setExtensionEggs] = useState<{ [x: string]: string[] }>( + blueprintExtensions.reduce((prev, current) => ({ ...prev, [current]: ['-1'] }), {}) + ); + + useEffect(() => { + (async () => { + const newEggs: { [x: string]: string[] } = {}; + for (const id of blueprintExtensions) { + const resp = await fetch(`/api/client/extensions/blueprint/eggs?${new URLSearchParams({ id })}`); + newEggs[id] = (await resp.json()) as string[]; + } + setExtensionEggs(newEggs); + })(); + }, []); + + return extensionEggs; +}; + export const NavigationLinks = () => { const rootAdmin = useStoreState((state) => state.user.data!.rootAdmin); const serverEgg = ServerContext.useStoreState((state) => state.server.data?.BlueprintFramework.eggId); @@ -22,10 +46,11 @@ export const NavigationLinks = () => { } return `${(url ? match.url : match.path).replace(/\/*$/, '')}/${value.replace(/^\/+/, '')}`; }; + const extensionEggs = useExtensionEggs(); + console.log(serverEgg); return ( <> - {/* Pterodactyl routes */} {routes.server .filter((route) => !!route.name) @@ -41,29 +66,31 @@ export const NavigationLinks = () => { {route.name} ) - ) - } + )} {/* Blueprint routes */} - {blueprintRoutes.server.length > 0 && blueprintRoutes.server - .filter((route) => !!route.name) - .filter((route) => route.adminOnly ? rootAdmin : true) - .filter((route) => route.eggs && serverEgg ? route.eggs.includes(serverEgg) : true ) - .map((route) => - route.permission ? ( - - + {blueprintRoutes.server.length > 0 && + blueprintRoutes.server + .filter((route) => !!route.name) + .filter((route) => (route.adminOnly ? rootAdmin : true)) + .filter((route) => + extensionEggs[route.identifier].includes('-1') + ? true + : extensionEggs[route.identifier].find((id) => id === serverEgg?.toString()) + ) + .map((route) => + route.permission ? ( + + + {route.name} + + + ) : ( + {route.name} - - ) : ( - - {route.name} - - ) - ) - } - + ) + )} ); }; @@ -78,13 +105,13 @@ export const NavigationRouter = () => { } return `${(url ? match.url : match.path).replace(/\/*$/, '')}/${value.replace(/^\/+/, '')}`; }; + const extensionEggs = useExtensionEggs(); const location = useLocation(); return ( <> - {/* Pterodactyl routes */} {routes.server.map(({ path, permission, component: Component }) => ( @@ -95,21 +122,25 @@ export const NavigationRouter = () => { ))} {/* Blueprint routes */} - {blueprintRoutes.server.length > 0 && blueprintRoutes.server - .filter((route) => route.adminOnly ? rootAdmin : true) - .filter((route) => route.eggs && serverEgg ? route.eggs.includes(serverEgg) : true ) - .map(({ path, permission, component: Component }) => ( - - - - - - )) - } + {blueprintRoutes.server.length > 0 && + blueprintRoutes.server + .filter((route) => (route.adminOnly ? rootAdmin : true)) + .filter((route) => + extensionEggs[route.identifier].includes('-1') + ? true + : extensionEggs[route.identifier].find((id) => id === serverEgg?.toString()) + ) + .map(({ path, permission, component: Component }) => ( + + + + + + ))} ); -}; \ No newline at end of file +}; diff --git a/resources/scripts/blueprint/extends/routers/routes.ts b/resources/scripts/blueprint/extends/routers/routes.ts index 225eef4..f51686a 100644 --- a/resources/scripts/blueprint/extends/routers/routes.ts +++ b/resources/scripts/blueprint/extends/routers/routes.ts @@ -2,7 +2,7 @@ import React from 'react'; /* blueprint/import */ -interface RouteDefinition { +interface RouteDefinition { path: string; name: string | undefined; component: React.ComponentType; @@ -12,7 +12,6 @@ interface RouteDefinition { } interface ServerRouteDefinition extends RouteDefinition { permission: string | string[] | null; - eggs?: number[]; } interface Routes { account: RouteDefinition[]; @@ -26,4 +25,4 @@ export default { server: [ /* routes/server */ ], -} as Routes; \ No newline at end of file +} as Routes;