From d6fc84381719b507b877b7874bfe9429aead92d7 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 5 Aug 2024 14:29:00 +0800 Subject: [PATCH] added install instructions for self managed extensions and package lookup endpoint --- addon/components/extension-card.js | 22 +++++++++ addon/components/extension-form.js | 22 +++++++++ addon/components/modals/extension-details.hbs | 3 ++ .../self-managed-install-instructions.hbs | 29 ++++++++++++ addon/styles/registry-bridge-engine.css | 28 +++++++++++ .../self-managed-install-instructions.js | 1 + .../Internal/v1/RegistryController.php | 46 +++++++++++++++++++ server/src/Models/RegistryExtension.php | 34 ++++++++++++++ server/src/routes.php | 3 +- .../self-managed-install-instructions-test.js | 26 +++++++++++ 10 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 addon/components/modals/self-managed-install-instructions.hbs create mode 100644 app/components/modals/self-managed-install-instructions.js create mode 100644 tests/integration/components/modals/self-managed-install-instructions-test.js diff --git a/addon/components/extension-card.js b/addon/components/extension-card.js index a34fb37..01edb90 100644 --- a/addon/components/extension-card.js +++ b/addon/components/extension-card.js @@ -64,6 +64,18 @@ export default class ExtensionCardComponent extends Component { stepDescription: 'Awaiting install to begin...', progress: 0, extension: this.extension, + viewSelfManagesInstallInstructions: () => { + const done = async () => { + await this.modalsManager.done(); + this.onClick(); + }; + + this.selfManagedInstallInstructions({ + extension: this.extension, + confirm: done, + decline: done, + }); + }, confirm: async (modal) => { modal.startLoading(); @@ -122,6 +134,16 @@ export default class ExtensionCardComponent extends Component { }); } + async selfManagedInstallInstructions(options = {}) { + await this.modalsManager.done(); + this.modalsManager.show('modals/self-managed-install-instructions', { + title: 'Install a Self Managed Extension', + hideDeclineButton: true, + acceptButtonText: 'Done', + ...options, + }); + } + async startCheckoutSession() { const checkout = await this.stripe.initEmbeddedCheckout({ fetchClientSecret: this.fetchClientSecret.bind(this), diff --git a/addon/components/extension-form.js b/addon/components/extension-form.js index 6dd80d1..e610943 100644 --- a/addon/components/extension-form.js +++ b/addon/components/extension-form.js @@ -145,8 +145,30 @@ export default class ExtensionFormComponent extends Component { acceptButtonDisabled: true, acceptButtonScheme: isPaymentRequired ? 'success' : 'primary', declineButtonText: 'Done', + viewSelfManagesInstallInstructions: () => { + const done = async () => { + await this.modalsManager.done(); + this.previewListing(); + }; + + this.selfManagedInstallInstructions({ + extension, + confirm: done, + decline: done, + }); + }, extension, ...options, }); } + + async selfManagedInstallInstructions(options = {}) { + await this.modalsManager.done(); + this.modalsManager.show('modals/self-managed-install-instructions', { + title: 'Install a Self Managed Extension', + hideDeclineButton: true, + acceptButtonText: 'Done', + ...options, + }); + } } diff --git a/addon/components/modals/extension-details.hbs b/addon/components/modals/extension-details.hbs index b7de2cc..15d2a9c 100644 --- a/addon/components/modals/extension-details.hbs +++ b/addon/components/modals/extension-details.hbs @@ -68,6 +68,9 @@ {{t "registry-bridge.component.extension-details-modal.self-managed" }} + {{#if @options.viewSelfManagesInstallInstructions}} + How to install + {{/if}} {{/if}} diff --git a/addon/components/modals/self-managed-install-instructions.hbs b/addon/components/modals/self-managed-install-instructions.hbs new file mode 100644 index 0000000..5ba55c3 --- /dev/null +++ b/addon/components/modals/self-managed-install-instructions.hbs @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/addon/styles/registry-bridge-engine.css b/addon/styles/registry-bridge-engine.css index 72e8c18..dcfb0f5 100644 --- a/addon/styles/registry-bridge-engine.css +++ b/addon/styles/registry-bridge-engine.css @@ -140,3 +140,31 @@ body[data-theme='dark'] .extension-card-container > .extension-card-body-contain margin-left: 2rem; margin-bottom: 2rem; } + +.self-managed-install-instructions { + list-style: decimal; + color: #000; + padding-left: 30px; + font-size: 1rem; +} + +.self-managed-install-instructions > li { + padding-bottom: 0.5rem; +} + +body[data-theme='dark'] .self-managed-install-instructions { + list-style: decimal; + color: #fff; + padding-left: 30px; +} + +.self-managed-install-instructions code { + font-family: monospace; + padding: 0.05rem 0.2rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 5%); + background-color: #030712; + border: 1px #1f2937 solid; + color: #22c55e; + border-radius: 0.25rem; + font-size: 0.75rem; +} diff --git a/app/components/modals/self-managed-install-instructions.js b/app/components/modals/self-managed-install-instructions.js new file mode 100644 index 0000000..3ec45df --- /dev/null +++ b/app/components/modals/self-managed-install-instructions.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/registry-bridge-engine/components/modals/self-managed-install-instructions'; diff --git a/server/src/Http/Controllers/Internal/v1/RegistryController.php b/server/src/Http/Controllers/Internal/v1/RegistryController.php index 124f2e6..4b563cf 100644 --- a/server/src/Http/Controllers/Internal/v1/RegistryController.php +++ b/server/src/Http/Controllers/Internal/v1/RegistryController.php @@ -6,6 +6,7 @@ use Fleetbase\Http\Resources\Category as CategoryResource; use Fleetbase\Models\Category; use Fleetbase\RegistryBridge\Models\RegistryExtension; +use Illuminate\Http\Request; class RegistryController extends Controller { @@ -51,4 +52,49 @@ public function getInstalledEngines() return response()->json($installedExtensions); } + + /** + * Lookup and retrieve package information based on the provided package name. + * + * This method handles a request to lookup a package by its name. It utilizes the `RegistryExtension::lookup` method to find the + * corresponding registry extension. If no extension is found or if the extension does not have valid package or composer data, + * an error response is returned. + * + * If a valid extension and its associated bundle are found, the function extracts the package and composer names from the + * `package.json` and `composer.json` metadata. These names are then returned in a JSON response. + * + * @param Request $request the incoming HTTP request containing the 'package' input parameter + * + * @return \Illuminate\Http\JsonResponse a JSON response containing the package and composer names if found, or an error message otherwise + */ + public function lookupPackage(Request $request) + { + $packageName = $request->input('package'); + $registryExtension = RegistryExtension::lookup($packageName); + if (!$registryExtension) { + return response()->error('No extension found by this name for install'); + } + + if (!$registryExtension->currentBundle) { + return response()->error('No valid package data found for this extension install'); + } + + $packageJson = $registryExtension->currentBundle->meta['package.json']; + if (!$packageJson) { + return response()->error('No valid package data found for this extension install'); + } + + $composerJson = $registryExtension->currentBundle->meta['composer.json']; + if (!$composerJson) { + return response()->error('No valid package data found for this extension install'); + } + + $packageJsonName = data_get($packageJson, 'name'); + $composerJsonName = data_get($composerJson, 'name'); + + return response()->json([ + 'npm' => $packageJsonName, + 'composer' => $composerJsonName, + ]); + } } diff --git a/server/src/Models/RegistryExtension.php b/server/src/Models/RegistryExtension.php index c9bb933..604d1ff 100644 --- a/server/src/Models/RegistryExtension.php +++ b/server/src/Models/RegistryExtension.php @@ -15,6 +15,7 @@ use Fleetbase\Traits\HasPublicId; use Fleetbase\Traits\HasUuid; use Fleetbase\Traits\Searchable; +use Illuminate\Support\Str; use Spatie\Sluggable\HasSlug; use Spatie\Sluggable\SlugOptions; @@ -444,6 +445,39 @@ public static function findByPackageName(string $packageName): ?RegistryExtensio })->first(); } + /** + * Lookup a registry extension based on the given package name. + * + * This method attempts to find a `RegistryExtension` that matches the provided package name. It checks multiple fields including + * `uuid`, `public_id`, and `slug`. If the package name starts with 'fleetbase/', it also attempts to match the slug extracted from the package name. + * + * Additionally, the method checks for the existence of a related `currentBundle` where the `package.json` or `composer.json` metadata + * matches the provided package name. + * + * @param string $packageName the name, UUID, public ID, or slug of the package to lookup + * + * @return RegistryExtension|null returns the found `RegistryExtension` instance or `null` if no match is found + */ + public static function lookup(string $packageName): ?RegistryExtension + { + return static::where('status', 'published')->where(function ($query) use ($packageName) { + $query->where('uuid', $packageName) + ->orWhere('public_id', $packageName) + ->orWhere('slug', $packageName); + + // Check for fleetbase/ prefix and match slug + if (Str::startsWith($packageName, 'fleetbase/')) { + $packageSlug = explode('/', $packageName)[1] ?? null; + if ($packageSlug) { + $query->orWhere('slug', $packageSlug); + } + } + })->orWhereHas('currentBundle', function ($query) use ($packageName) { + $query->where('meta->package.json->name', $packageName) + ->orWhere('meta->composer.json->name', $packageName); + })->with(['currentBundle'])->first(); + } + /** * Determines if the current extension instance is ready for submission. * diff --git a/server/src/routes.php b/server/src/routes.php index b475c5a..034becc 100644 --- a/server/src/routes.php +++ b/server/src/routes.php @@ -12,7 +12,8 @@ | is assigned the "api" middleware group. Enjoy building your API! | */ - +// Lookup package endpoint +Route::get(config('internals.api.routing.prefix', '~registry') . '/v1/lookup', 'Fleetbase\RegistryBridge\Http\Controllers\Internal\v1\RegistryController@lookupPackage'); Route::prefix(config('internals.api.routing.prefix', '~registry'))->middleware(['fleetbase.registry'])->namespace('Fleetbase\RegistryBridge\Http\Controllers')->group( function ($router) { /* diff --git a/tests/integration/components/modals/self-managed-install-instructions-test.js b/tests/integration/components/modals/self-managed-install-instructions-test.js new file mode 100644 index 0000000..67288ca --- /dev/null +++ b/tests/integration/components/modals/self-managed-install-instructions-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'dummy/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | modals/self-managed-install-instructions', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +});