chore: wiki and artificats cleanups (#56)

* chore: document OpenAPI support

* chore: update readme

* refactor: use scrollArea wherever applicable

* chore: update demo video

* build: exclude more files from the release

* style: apply TS style fixes
This commit is contained in:
Mazen Touati
2026-02-02 02:31:42 +01:00
committed by GitHub
parent 23af80288e
commit e1fe4eefeb
13 changed files with 156 additions and 60 deletions

3
.gitattributes vendored
View File

@@ -6,6 +6,7 @@
/.gitattributes export-ignore /.gitattributes export-ignore
/.gitignore export-ignore /.gitignore export-ignore
/.editorconfig export-ignore /.editorconfig export-ignore
/.storybook export-ignore
/art export-ignore /art export-ignore
/wiki export-ignore /wiki export-ignore
/bin export-ignore /bin export-ignore
@@ -24,3 +25,5 @@
/package.json export-ignore /package.json export-ignore
/package-lock.json export-ignore /package-lock.json export-ignore
/CONTRIBUTING.md export-ignore /CONTRIBUTING.md export-ignore
/.release-please-manifest.json export-ignore
/.release-please-config.json export-ignore

View File

@@ -20,49 +20,47 @@ Traditional API testing tools require manual setup for every endpoint. Nimbus re
### What Nimbus Is NOT ### What Nimbus Is NOT
Nimbus is **NOT** an API documentation generator. It doesn't produce client-facing API documentation. Instead, it's a **developer-focused API playground** designed to improve your development experience while building and testing APIs. Nimbus is **NOT** an API documentation generator like Swagger or Scribe. It doesn't produce customer-facing API documentation. Instead, it's a **developer-focused API playground** designed to improve your iteration speed while building and testing APIs.
## Features ## Key Features
- Automatic route and schema discovery from Laravel routes and validation rules. - Automatic Discovery: Routes and schemas generated directly from your Laravel `FormRequest`, `SpatieData` classes and inline validation rules.
- Built-in, polished interface for testing API endpoints in your browser. - Built-in, polished interface for inspecting API endpoints in your browser.
- Response viewer with JSON formatting and syntax highlighting. - Shareable Links: Capture a request state (headers, body, auth) and send it to a colleague.
- Cookie inspection with automatic Laravel cookie decryption. - Safe Testing (Transaction Mode): Run a request and automatically roll back database changes. Test `DELETE` or `UPDATE` endpoints without dirtying your data.
- Special authentication modes: - OpenAPI as a First-Class Citizen: Use your OpenAPI schema to super-charge discovery while retaining Nimbus's automatic detection for undocumented routes.
- Make requests as the currently logged-in user. - Magic `dd()` Handling: Intercepts `dd()` calls and renders them in a paginated window that doesn't break your UI.
- Impersonate other users by ID. - Multi-Application Support: Switch between different APIs (e.g., `rest-api`, `admin-api`) within the same interface.
- Bearer token and Basic Auth support. - Special Authentication:
- Inline value generators for realistic test data (UUIDs, emails, names, dates, etc.). - Act as the currently logged-in user.
- Impersonate any user by ID.
- Bearer and Basic Auth support.
- Global headers automatically applied to every request. - Global headers automatically applied to every request.
- One-click payload population with realistic test data. - Value Generators: One-click payload population with realistic test data (UUIDs, names, emails, etc.).
- Export requests as cURL commands.
--- ---
## Quick Start ## Quick Start
### Requirements ### 1. Requirements
- PHP 8.2 or higher. - PHP 8.2+
- Laravel 10.x, 11.x, or 12.x. - Laravel 10.x, 11.x, or 12.x
- A real web server (Herd, Sail, Docker, Nginx). *Note: `php artisan serve` is not supported.*
### Installation ### 2. Installation
Install via Composer:
```bash ```bash
composer require sunchayn/nimbus composer require sunchayn/nimbus
``` ```
Publish the configuration and assets: ### 3. Setup
```bash ```bash
php artisan vendor:publish --tag=nimbus-assets --tag=nimbus-config php artisan vendor:publish --tag=nimbus-assets --tag=nimbus-config
``` ```
This publishes `config/nimbus.php` and the necessary frontend assets. ### 4. Access Nimbus
### Access Nimbus
Start your Laravel application and navigate to: Start your Laravel application and navigate to:
@@ -85,21 +83,12 @@ That's it! Nimbus will automatically discover your API routes and their validati
## Alpha Release Notice ## Alpha Release Notice
Nimbus is currently an **alpha** release to validate the concept. You may encounter unexpected behaviors or bugs. All feedback is welcome: Nimbus is currently an **alpha**. You may encounter unexpected behaviors or bugs. All feedback is welcome:
- Report bugs: [Open an issue](https://github.com/sunchayn/nimbus/issues/new/choose) - Report bugs: [Open an issue](https://github.com/sunchayn/nimbus/issues/new/choose)
- Share ideas: [Start a discussion](https://github.com/sunchayn/nimbus/discussions/categories/ideas) - Share ideas: [Start a discussion](https://github.com/sunchayn/nimbus/discussions/categories/ideas)
- Ask questions: [Q&A discussions](https://github.com/sunchayn/nimbus/discussions/categories/q-a) - Ask questions: [Q&A discussions](https://github.com/sunchayn/nimbus/discussions/categories/q-a)
## Contributing
Thanks for considering contributing! Please read the [Contributor Guide](wiki/contribution-guide/README.md) for the following:
- Architecture overview and design principles.
- Development environment setup.
- Coding standards and testing guidelines.
- Pull request process.
## License ## License
Nimbus is open-source software licensed under the [MIT license](LICENSE.md). Nimbus is open-source software licensed under the [MIT license](LICENSE.md).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 4.8 MiB

View File

@@ -29,6 +29,23 @@
[role='button']:disabled { [role='button']:disabled {
cursor: default; cursor: default;
} }
/*
* Global Scrollbar Styling.
*/
* {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}
*:hover {
scrollbar-color: var(--color-zinc-400) transparent;
}
.dark *:hover {
scrollbar-color: var(--color-zinc-600) transparent;
}
} }
@layer utilities { @layer utilities {

View File

@@ -7,7 +7,7 @@ import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core'; import { reactiveOmit } from '@vueuse/core';
import type { ScrollAreaRootProps } from 'reka-ui'; import type { ScrollAreaRootProps } from 'reka-ui';
import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from 'reka-ui'; import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from 'reka-ui';
import type { HTMLAttributes } from 'vue'; import { type HTMLAttributes, ref } from 'vue';
import AppScrollBar from './AppScrollBar.vue'; import AppScrollBar from './AppScrollBar.vue';
/* /*
@@ -23,8 +23,13 @@ export interface AppScrollAreaProps extends ScrollAreaRootProps {
*/ */
const props = defineProps<AppScrollAreaProps>(); const props = defineProps<AppScrollAreaProps>();
const delegatedProps = reactiveOmit(props, 'class'); const delegatedProps = reactiveOmit(props, 'class');
const viewport = ref<HTMLElement | null>(null);
defineExpose({
viewport,
});
</script> </script>
<template> <template>
@@ -34,6 +39,7 @@ const delegatedProps = reactiveOmit(props, 'class');
:class="cn('relative', props.class)" :class="cn('relative', props.class)"
> >
<ScrollAreaViewport <ScrollAreaViewport
ref="viewport"
data-slot="scroll-area-viewport" data-slot="scroll-area-viewport"
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:h-full" class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:h-full"
> >

View File

@@ -3,6 +3,7 @@
* @component RequestBodyContent * @component RequestBodyContent
* @description Dynamic content renderer for the request body based on the selected payload type. * @description Dynamic content renderer for the request body based on the selected payload type.
*/ */
import { AppScrollArea } from '@/components/base/scroll-area';
import { RequestBodyTypeEnum } from '@/interfaces/http'; import { RequestBodyTypeEnum } from '@/interfaces/http';
import type { JSONSchema7 } from 'json-schema'; import type { JSONSchema7 } from 'json-schema';
import RequestBodyFormData from './RequestBodyFormData.vue'; import RequestBodyFormData from './RequestBodyFormData.vue';
@@ -42,7 +43,7 @@ const updatePayload = (value: FormData | string | null) => {
</script> </script>
<template> <template>
<div class="min-h-0 w-full flex-1"> <AppScrollArea class="min-h-0 w-full flex-1">
<RequestBodyJson <RequestBodyJson
v-if="payloadType === RequestBodyTypeEnum.JSON" v-if="payloadType === RequestBodyTypeEnum.JSON"
:model-value="payload as string" :model-value="payload as string"
@@ -60,5 +61,5 @@ const updatePayload = (value: FormData | string | null) => {
@update:model-value="updatePayload" @update:model-value="updatePayload"
/> />
<RequestBodyFormNone v-else @update:model-value="updatePayload" /> <RequestBodyFormNone v-else @update:model-value="updatePayload" />
</div> </AppScrollArea>
</template> </template>

View File

@@ -27,5 +27,6 @@ defineProps<AppResponseBodyProps>();
:placeholder="content === '' ? 'Empty' : 'Response JSON Payload'" :placeholder="content === '' ? 'Empty' : 'Response JSON Payload'"
:model-value="content" :model-value="content"
:disabled="content === ''" :disabled="content === ''"
:auto-height="false"
/> />
</template> </template>

View File

@@ -4,6 +4,7 @@
* @description Renders the successful response details, including body, headers, and cookies. * @description Renders the successful response details, including body, headers, and cookies.
*/ */
import { AppBadge } from '@/components/base/badge'; import { AppBadge } from '@/components/base/badge';
import { AppScrollArea } from '@/components/base/scroll-area';
import { import {
AppTabs, AppTabs,
AppTabsContent, AppTabsContent,
@@ -141,11 +142,15 @@ const handleTabClick = (event: Event) => {
value="response" value="response"
class="mt-0 flex min-h-0 flex-1 flex-col overflow-hidden" class="mt-0 flex min-h-0 flex-1 flex-col overflow-hidden"
> >
<ResponseBody <AppScrollArea
v-if="lastLog?.response?.status !== STATUS.DUMP_AND_DIE" v-if="lastLog?.response?.status !== STATUS.DUMP_AND_DIE"
class="min-h-0 overflow-auto" class="min-h-0 flex-1"
:content="lastLog?.response?.body ?? ''" >
/> <ResponseBody
class="min-h-0"
:content="lastLog?.response?.body ?? ''"
/>
</AppScrollArea>
<ResponseDumpAndDie <ResponseDumpAndDie
v-else v-else

View File

@@ -23,6 +23,7 @@ export interface AppCodeEditorProps extends PrimitiveProps {
readonly?: boolean; readonly?: boolean;
disabled?: boolean; disabled?: boolean;
validationSchema?: JSONSchema7; validationSchema?: JSONSchema7;
autoHeight?: boolean;
} }
/* /*
@@ -35,6 +36,7 @@ const props = withDefaults(defineProps<AppCodeEditorProps>(), {
disabled: false, disabled: false,
class: '', class: '',
validationSchema: undefined, validationSchema: undefined,
autoHeight: false,
}); });
const model = defineModel<string>({ const model = defineModel<string>({
@@ -61,7 +63,7 @@ const updateModel = (value: string) => {
<template> <template>
<Codemirror <Codemirror
:placeholder="placeholder" :placeholder="placeholder"
:style="{ height: '100%' }" :style="{ height: autoHeight ? 'auto' : '100%', minHeight: '100%' }"
:extensions="extensions" :extensions="extensions"
:indent-with-tab="true" :indent-with-tab="true"
:tab-size="4" :tab-size="4"

View File

@@ -8,6 +8,7 @@ import {
AppCollapsibleContent, AppCollapsibleContent,
AppCollapsibleTrigger, AppCollapsibleTrigger,
} from '@/components/base/collapsible'; } from '@/components/base/collapsible';
import { AppScrollArea } from '@/components/base/scroll-area';
import { import {
AppSidebarGroup, AppSidebarGroup,
AppSidebarGroupContent, AppSidebarGroupContent,
@@ -31,11 +32,16 @@ const tabsStore = useTabsStore();
* Vertical Scroll. * Vertical Scroll.
*/ */
const { scrollContainer, showTopMask, showBottomMask, scrollTabIntoView } = const {
useTabVerticalScroll({ scrollContainer,
SCROLL_PADDING: 20, showTopMask,
MASK_HEIGHT: 32, showBottomMask,
}); updateScrollMasks,
scrollTabIntoView,
} = useTabVerticalScroll({
SCROLL_PADDING: 20,
MASK_HEIGHT: 32,
});
const tabElements = ref<Record<string, HTMLElement>>({}); const tabElements = ref<Record<string, HTMLElement>>({});
@@ -96,6 +102,16 @@ const handleCloseTab = (id: string) => {
*/ */
const isOpen = defineModel<boolean>('isOpen'); const isOpen = defineModel<boolean>('isOpen');
const scrollAreaRef = ref<InstanceType<typeof AppScrollArea> | null>(null);
watch(
() => scrollAreaRef.value?.viewport,
viewport => {
if (viewport) {
scrollContainer.value = viewport;
}
},
);
</script> </script>
<template> <template>
@@ -136,9 +152,10 @@ const isOpen = defineModel<boolean>('isOpen');
:class="[showTopMask && isOpen ? 'opacity-100' : 'opacity-0']" :class="[showTopMask && isOpen ? 'opacity-100' : 'opacity-0']"
/> />
<div <AppScrollArea
ref="scrollContainer" ref="scrollAreaRef"
class="no-scrollbar min-h-0 flex-1 overflow-y-scroll" class="min-h-0 flex-1"
@scroll="updateScrollMasks"
> >
<draggable <draggable
v-model="tabsModel" v-model="tabsModel"
@@ -177,7 +194,7 @@ const isOpen = defineModel<boolean>('isOpen');
</AppSidebarMenuItem> </AppSidebarMenuItem>
</template> </template>
</draggable> </draggable>
</div> </AppScrollArea>
<!-- Bottom Scroll Mask --> <!-- Bottom Scroll Mask -->
<div <div

View File

@@ -8,6 +8,7 @@ import {
AppResizablePanel, AppResizablePanel,
AppResizablePanelGroup, AppResizablePanelGroup,
} from '@/components/base/resizable'; } from '@/components/base/resizable';
import { AppScrollArea } from '@/components/base/scroll-area';
import { import {
AppSidebar, AppSidebar,
AppSidebarContent, AppSidebarContent,
@@ -60,6 +61,8 @@ const isOpenTabsExpanded = useStorage(
false, false,
); );
const scrollAreaRef = ref<InstanceType<typeof AppScrollArea> | null>(null);
const { const {
scrollContainer: routesScrollContainer, scrollContainer: routesScrollContainer,
showTopMask: showRoutesTopMask, showTopMask: showRoutesTopMask,
@@ -148,6 +151,15 @@ watch(isOpenTabsExpanded, newValue => {
} }
}); });
watch(
() => scrollAreaRef.value?.viewport,
viewport => {
if (viewport) {
routesScrollContainer.value = viewport;
}
},
);
provide('showingSearchResults', showingSearchResults); provide('showingSearchResults', showingSearchResults);
</script> </script>
@@ -207,9 +219,9 @@ provide('showingSearchResults', showingSearchResults);
<AppSidebarGroupContent <AppSidebarGroupContent
class="relative min-h-0 flex-1 overflow-hidden" class="relative min-h-0 flex-1 overflow-hidden"
> >
<div <AppScrollArea
ref="routesScrollContainer" ref="scrollAreaRef"
class="h-full overflow-y-auto" class="h-full"
@scroll="updateRoutesScrollMasks" @scroll="updateRoutesScrollMasks"
> >
<div <div
@@ -228,12 +240,11 @@ provide('showingSearchResults', showingSearchResults);
</p> </p>
</div> </div>
</AppSidebarMenu> </AppSidebarMenu>
</div> <div
v-show="showRoutesBottomMask"
<div class="from-sidebar pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-8 bg-gradient-to-t from-20% to-transparent transition-opacity duration-300"
v-show="showRoutesBottomMask" />
class="from-sidebar pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-8 bg-gradient-to-t from-20% to-transparent transition-opacity duration-300" </AppScrollArea>
/>
</AppSidebarGroupContent> </AppSidebarGroupContent>
</AppSidebarGroup> </AppSidebarGroup>
</AppResizablePanel> </AppResizablePanel>

View File

@@ -31,6 +31,7 @@ This guide covers everything you need to know about using Nimbus to test and exp
- [Transaction Mode](#transaction-mode) - [Transaction Mode](#transaction-mode)
- [Export to cURL](#export-to-curl) - [Export to cURL](#export-to-curl)
- [Shareable Links](#shareable-links) - [Shareable Links](#shareable-links)
- [OpenAPI Support](#openapi-support)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Getting Help](#getting-help) - [Getting Help](#getting-help)
@@ -397,6 +398,46 @@ When a shareable link is opened, Nimbus will:
- Automatically restore all request data. - Automatically restore all request data.
- **Switch Applications**: If the link points to a route in a different application (e.g., from `Rest API` to `Admin API`), Nimbus will automatically switch the active application context for you. - **Switch Applications**: If the link points to a route in a different application (e.g., from `Rest API` to `Admin API`), Nimbus will automatically switch the active application context for you.
### OpenAPI Support
By default, Nimbus auto-detects routes from your Laravel application. However, you can also configure Nimbus to load routes directly from OpenAPI specification files.
**Prerequisites:**
This feature requires the `devizzent/cebe-php-openapi` package:
```bash
composer require devizzent/cebe-php-openapi
```
**Configuration:**
To enable OpenAPI support, update your `config/nimbus.php`:
1. Set the strategy to OpenAPI:
```php
'strategy' => \Sunchayn\Nimbus\Modules\Config\Enums\RoutesProcessingStrategyEnum::OpenAPI,
```
2. Configure the `openapi` section:
```php
'openapi' => [
'files' => [
// For unversioned APIs
'default' => base_path('docs/openapi.yaml'),
// OR for versioned APIs (keys must match version segments)
'v1' => base_path('docs/v1/openapi.yaml'),
'v2' => base_path('docs/v2/openapi.json'),
],
// Toggle display of Operation IDs in the sidebar instead of the route path
'show_operation_id' => false,
],
```
![OpenAPI Support](./assets/open-api-support.png)
--- ---
## Configuration ## Configuration
@@ -436,6 +477,9 @@ return [
| **`applications.*.routes.prefix`** | The base path used to detect application routes. Only routes starting with this prefix are analyzed. | `'api'` | `'api/v1'` | | **`applications.*.routes.prefix`** | The base path used to detect application routes. Only routes starting with this prefix are analyzed. | `'api'` | `'api/v1'` |
| **`applications.*.routes.versioned`** | Enables version parsing for routes like `/api/v1/...`. | `false` | `true` | | **`applications.*.routes.versioned`** | Enables version parsing for routes like `/api/v1/...`. | `false` | `true` |
| **`applications.*.routes.api_base_url`** | The base URL used when Nimbus relays API requests from the UI. Useful when the API runs on a different domain or port. If set to null, Nimbus will default to the same host and scheme as the incoming request. | null | `http://127.0.0.1:8001` | | **`applications.*.routes.api_base_url`** | The base URL used when Nimbus relays API requests from the UI. Useful when the API runs on a different domain or port. If set to null, Nimbus will default to the same host and scheme as the incoming request. | null | `http://127.0.0.1:8001` |
| **`applications.*.routes.strategy`** | The strategy used to discover routes. Options: `AutoDetect` or `OpenAPI`. | `AutoDetect` | `RoutesProcessingStrategyEnum::OpenAPI` |
| **`applications.*.routes.openapi.files`** | Map of versions to OpenAPI file paths. Required when strategy is `OpenAPI`. | `[]` | `['v1' => base_path('docs/v1.yaml')]` |
| **`applications.*.routes.openapi.show_operation_id`** | Defines whether to show the Operation ID in the sidebar. | `false` | `true` |
| **`applications.*.auth.guard`** | The Laravel guard used for the API requests authentication. | `'api'` | `'web'` | | **`applications.*.auth.guard`** | The Laravel guard used for the API requests authentication. | `'api'` | `'web'` |
| **`applications.*.auth.special.injector`** | Injector class used to attach authentication credentials to outgoing requests. Must implement `SpecialAuthenticationInjectorContract`. | `RememberMeCookieInjector::class` | `TymonJwtTokenInjector::class` | | **`applications.*.auth.special.injector`** | Injector class used to attach authentication credentials to outgoing requests. Must implement `SpecialAuthenticationInjectorContract`. | `RememberMeCookieInjector::class` | `TymonJwtTokenInjector::class` |
| **`headers`** | Global headers applied to all outgoing requests. Supports static values or enum generators. | `[]` | `['x-request-id' => GlobalHeaderGeneratorTypeEnum::UUID]` | | **`headers`** | Global headers applied to all outgoing requests. Supports static values or enum generators. | `[]` | `['x-request-id' => GlobalHeaderGeneratorTypeEnum::UUID]` |

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB