# ProviderSwitcher

> Lists an app's swappable capabilities (data, queue, cache, storage…) with the backend each runs on, and switches backends at runtime.

Source: https://docs.nasaqui.com/components/provider-switcher

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/provider-switcher.json
```

An operator panel for apps built on swappable capabilities. Each row is one capability (data, queue, cache, storage, realtime…) with the backend it runs on, and a select to switch it. Rows on the app's configured default say **Default**; switched rows say **Overridden**. Rows pinned by config are locked.

## When to use

- In an admin or developer settings page of a framework app whose drivers can change at runtime (the ToGO provider panel).

## When not to use

- Plain environment variables: use [`EnvList`](https://docs.nasaqui.com/components/env-list).
- On/off switches for product features: use [`FeatureFlags`](https://docs.nasaqui.com/components/feature-flags).

## Import

```tsx
import { ProviderSwitcher } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { ProviderSwitcher } from "@fadymondy/nasaq/web";

<ProviderSwitcher
  capabilities={[
    { capability: "data", label: "Data", active: "postgres", options: ["postgres", "sqlite"], isDefault: true },
    { capability: "queue", label: "Queue", active: "redis", options: ["redis", "nats", "database"], isDefault: false },
  ]}
  onSelect={(capability, backend) => api.post(`/providers/${capability}`, { backend })}
/>;
```

## Anatomy

```
div [data-slot=provider-switcher]
├─ header row (Capability · Backend), from sm up
└─ ul
   └─ li [data-slot=provider-switcher-row][data-capability][aria-busy]
      ├─ label + Status (Default / Overridden) + description
      └─ Select [data-slot=provider-switcher-select]
```

## API

### `ProviderSwitcher`

| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `capabilities` | `ProviderCapability[]` | — | Required. One row each. An empty list shows a short message. |
| `onSelect` | `(capability, backend) => void \| Promise<void>` | — | Leave out for a read-only view. A returned promise keeps the row busy until it settles. |
| `labels` | `Partial<ProviderSwitcherLabels>` | — | Override the built-in strings. |

Plus any `<div>` prop.

### `ProviderCapability`

| Field | Type | Notes |
| --- | --- | --- |
| `capability` | `string` | The key sent to `onSelect`: `"data"`, `"queue"`. |
| `label` / `description` | `ReactNode` | Name and one line of what it does. Default name is the key. |
| `active` | `string` | The backend in use. |
| `options` | `(string \| ProviderOption)[]` | Backends to choose from. |
| `isDefault` | `boolean` | `true` shows Default, `false` shows Overridden, unset shows nothing. |
| `locked` | `boolean` | Disables the select (pinned by config). |

### `ProviderOption`

| Field | Type | Notes |
| --- | --- | --- |
| `id` | `string` | The backend id. |
| `label` / `description` | `ReactNode` | Menu text. |
| `disabled` | `boolean` | Listed but not selectable. |

## Examples

### Rich options

```tsx
{
  capability: "data",
  active: "postgres",
  options: [
    { id: "postgres", label: "PostgreSQL" },
    { id: "sqlite", label: "SQLite", description: "Local file, for development" },
  ],
}
```

### Read-only overview

```tsx
<ProviderSwitcher capabilities={capabilities} />
```

## Accessibility

- Each select is named "Backend for Data" / "مزوّد البيانات" after its row.
- A row that is applying a change has `aria-busy` and its select is disabled.
- Default and Overridden use `Status`, which pairs a shape with the colour.

## RTL & i18n

Strings ship in English and Arabic. Capability and backend names come from the host; backend ids such as `postgres` stay Latin.

## Styling & tokens

- `rounded-card border-border bg-card` surface with `divide-border` rows; `text-label` names, `text-caption` descriptions.
- Target a row with `[data-slot=provider-switcher-row][data-capability=queue]`.

## Do / Don't

- Do confirm a switch server-side and return the promise, so the row shows the change only once it applied.
- Do lock capabilities that can't change without a restart.
- Don't list backends the app has no driver for.

## Related

`env-list`, `feature-flags`, `select`, `status`.

## Lab

https://docs.nasaqui.com/?path=/docs/components-developer-tools-provider-switcher--docs

## Code

### React

```tsx
import { ProviderSwitcher } from "@fadymondy/nasaq/web";

<ProviderSwitcher
  capabilities={[
    { capability: "data", label: "Data", active: "postgres", options: ["postgres", "sqlite"], isDefault: true },
    { capability: "queue", label: "Queue", active: "redis", options: ["redis", "nats", "database"], isDefault: false },
  ]}
  onSelect={(capability, backend) => api.post(`/providers/${capability}`, { backend })}
/>;
```

### shadcn

```tsx
import { ProviderSwitcher } from "@/components/ui/provider-switcher";

<ProviderSwitcher
  capabilities={[
    { capability: "data", label: "Data", active: "postgres", options: ["postgres", "sqlite"], isDefault: true },
    { capability: "queue", label: "Queue", active: "redis", options: ["redis", "nats", "database"], isDefault: false },
  ]}
  onSelect={(capability, backend) => api.post(`/providers/${capability}`, { backend })}
/>;
```

### Vue

```vue
<script setup lang="ts">
import { NqProviderSwitcher } from "@fadymondy/nasaq/vue";

const capabilities = [
  { capability: "data", label: "Data", active: "postgres", options: ["postgres", "sqlite"], isDefault: true },
  { capability: "queue", label: "Queue", active: "redis", options: ["redis", "nats", "database"], isDefault: false },
];

function onSelect(capability: string, backend: string) {
  return new Promise<void>((resolve) => setTimeout(resolve, 400)).then(() => console.log(capability, backend));
}
</script>

<template>
  <NqProviderSwitcher :capabilities="capabilities" @select="onSelect" />
</template>
```

### Blade

```blade
<x-nq::provider-switcher :capabilities="[
    ['capability' => 'data', 'label' => 'Data', 'active' => 'postgres', 'options' => ['postgres', 'sqlite'], 'isDefault' => true],
    ['capability' => 'queue', 'label' => 'Queue', 'active' => 'redis', 'options' => ['redis', 'nats', 'database'], 'isDefault' => false],
]" />
```

### HTML + Alpine

```html
<div data-slot="provider-switcher" x-data="nqProviderSwitcher(JSON.parse('{\u0022data\u0022:{\u0022active\u0022:\u0022postgres\u0022,\u0022isDefault\u0022:true},\u0022queue\u0022:{\u0022active\u0022:\u0022redis\u0022,\u0022isDefault\u0022:false}}'))" class="overflow-hidden rounded-card border border-border bg-card">
        <div aria-hidden="true" class="hidden grid-cols-[minmax(0,1fr)_minmax(10rem,14rem)] gap-4 border-b border-border px-4 py-2 text-caption text-muted-foreground sm:grid">
            <span>Capability</span>
            <span>Backend</span>
        </div>
        <ul class="divide-y divide-border">
                                            <li data-slot="provider-switcher-row" data-capability="data" :aria-busy="busy === 'data' ? 'true' : undefined"
                    class="grid gap-2 px-4 py-3 sm:grid-cols-[minmax(0,1fr)_minmax(10rem,14rem)] sm:items-center sm:gap-4">
                    <div class="flex min-w-0 flex-col gap-0.5">
                        <div class="flex flex-wrap items-center gap-2">
                            <span class="text-label text-foreground">Data</span>
                                                            <span x-show="rows['data'].isDefault === true" x-cloak  class="contents"><span data-slot="status" data-tone="neutral" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
            <svg aria-hidden="true" class="size-3.5 shrink-0 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
</svg>        <span class="truncate"  title="Default" >Default</span>
</span>
</span>
                                <span x-show="rows['data'].isDefault === false" x-cloak  style="display: none"  class="contents"><span data-slot="status" data-tone="info" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
            <svg aria-hidden="true" class="size-3.5 shrink-0 text-nq-info-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <circle cx="12" cy="12" r="1"/>
</svg>        <span class="truncate"  title="Overridden" >Overridden</span>
</span>
</span>
                                                    </div>
                                            </div>
                    <div data-slot="select" x-data="nqSelect('postgres', false)" x-modelable="value" x-id="['nq-select']" x-model="rows[&#039;data&#039;].active" class="contents">
    <button data-slot="provider-switcher-select" x-ref="trigger" x-bind="trigger" aria-label="Backend for Data"
                            :disabled="busy === 'data'"                             class="flex h-control min-w-0 items-center justify-between gap-2 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] cursor-default select-none outline-none transition-colors duration-150 ease-nq focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-popup-open:border-nq-focus data-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] w-full">
                            <span data-slot="select-value" x-text="JSON.parse('{\u0022postgres\u0022:\u0022postgres\u0022,\u0022sqlite\u0022:\u0022sqlite\u0022}')[value] ?? value" class="min-w-0 flex-1 truncate text-start">postgres</span>
                            <span class="flex shrink-0 text-muted-foreground [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg></span>
                        </button>
                        <template x-teleport="body">
    <div data-slot="select-content" x-ref="popup" x-bind="popup" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.trigger"
        class="z-50 min-w-[var(--anchor-width)] max-h-[var(--available-height)] overflow-y-auto rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
        <div data-slot="select-item" x-bind="item('postgres', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('postgres')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate"><span class="flex flex-col">
                                        <span>postgres</span>
                                                                            </span></span>
</div>
                                                            <div data-slot="select-item" x-bind="item('sqlite', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('sqlite')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate"><span class="flex flex-col">
                                        <span>sqlite</span>
                                                                            </span></span>
</div>
    </div>
</template>
    </div>
                </li>
                                            <li data-slot="provider-switcher-row" data-capability="queue" :aria-busy="busy === 'queue' ? 'true' : undefined"
                    class="grid gap-2 px-4 py-3 sm:grid-cols-[minmax(0,1fr)_minmax(10rem,14rem)] sm:items-center sm:gap-4">
                    <div class="flex min-w-0 flex-col gap-0.5">
                        <div class="flex flex-wrap items-center gap-2">
                            <span class="text-label text-foreground">Queue</span>
                                                            <span x-show="rows['queue'].isDefault === true" x-cloak  style="display: none"  class="contents"><span data-slot="status" data-tone="neutral" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
            <svg aria-hidden="true" class="size-3.5 shrink-0 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
</svg>        <span class="truncate"  title="Default" >Default</span>
</span>
</span>
                                <span x-show="rows['queue'].isDefault === false" x-cloak  class="contents"><span data-slot="status" data-tone="info" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
            <svg aria-hidden="true" class="size-3.5 shrink-0 text-nq-info-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <circle cx="12" cy="12" r="1"/>
</svg>        <span class="truncate"  title="Overridden" >Overridden</span>
</span>
</span>
                                                    </div>
                                            </div>
                    <div data-slot="select" x-data="nqSelect('redis', false)" x-modelable="value" x-id="['nq-select']" x-model="rows[&#039;queue&#039;].active" class="contents">
    <button data-slot="provider-switcher-select" x-ref="trigger" x-bind="trigger" aria-label="Backend for Queue"
                            :disabled="busy === 'queue'"                             class="flex h-control min-w-0 items-center justify-between gap-2 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] cursor-default select-none outline-none transition-colors duration-150 ease-nq focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-popup-open:border-nq-focus data-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] w-full">
                            <span data-slot="select-value" x-text="JSON.parse('{\u0022redis\u0022:\u0022redis\u0022,\u0022nats\u0022:\u0022nats\u0022,\u0022database\u0022:\u0022database\u0022}')[value] ?? value" class="min-w-0 flex-1 truncate text-start">redis</span>
                            <span class="flex shrink-0 text-muted-foreground [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg></span>
                        </button>
                        <template x-teleport="body">
    <div data-slot="select-content" x-ref="popup" x-bind="popup" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.trigger"
        class="z-50 min-w-[var(--anchor-width)] max-h-[var(--available-height)] overflow-y-auto rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
        <div data-slot="select-item" x-bind="item('redis', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('redis')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate"><span class="flex flex-col">
                                        <span>redis</span>
                                                                            </span></span>
</div>
                                                            <div data-slot="select-item" x-bind="item('nats', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('nats')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate"><span class="flex flex-col">
                                        <span>nats</span>
                                                                            </span></span>
</div>
                                                            <div data-slot="select-item" x-bind="item('database', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('database')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate"><span class="flex flex-col">
                                        <span>database</span>
                                                                            </span></span>
</div>
    </div>
</template>
    </div>
                </li>
                    </ul>
    </div>
```
