Inserts StepMarketplace between StepComponents and StepDomain so the operator can opt the new Sovereign into a multi-tenant SaaS platform during provisioning. The toggle drives store.marketplaceEnabled, which StepReview now ships in the POST /v1/deployments body — the catalyst-api Request struct + OpenTofu var.marketplace_enabled + cloud-init Flux substitute + bp-catalyst-platform ingress.marketplace.enabled values were all wired earlier (PR #719); this PR is the missing UI seam. Brand fields (name / tagline / primary colour) persist on the wizard state so a future settings page can read them without re-prompting on every wizard run. The chart only consumes the enabled flag for now. Wizard step list grows from 7 to 8 stops (StepMarketplace at id=6, shifting Domain → 7 and Review → 8). WizardLayout test updated to assert the new count; the existing pre-existing StepComponents test failures (CORTEX cascade) and the @tabler/icons-react typecheck error are untouched and unrelated. Companion PRs (other agents): post-launch settings page + catalog publish/unpublish admin. This is 1 of 3 parallel pieces on #710 wave 3. Co-authored-by: hatiyildiz <hatiyildiz@openova.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f7365de162
commit
dad5ead534
@ -14,8 +14,9 @@
|
||||
* • The OpenOva logo lives inside the header (anchored on the brand
|
||||
* `Link`).
|
||||
*
|
||||
* • Exactly seven step indicators render (StepOrg → StepReview),
|
||||
* matching the wizard's seven-step waterfall.
|
||||
* • Exactly eight step indicators render (StepOrg → StepReview),
|
||||
* matching the wizard's eight-step waterfall (Marketplace step
|
||||
* was inserted between Components and Domain — issue #710 wave 3a).
|
||||
*
|
||||
* • The active step gets the `active` class and `aria-current="step"`,
|
||||
* so screen readers and visual regression tests both confirm the
|
||||
@ -107,14 +108,15 @@ describe('WizardLayout — page-header refactor (#174)', () => {
|
||||
expect(within(header).getByText('OpenOva')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders exactly seven step indicators inside the header', async () => {
|
||||
it('renders exactly eight step indicators inside the header', async () => {
|
||||
renderLayout()
|
||||
const header = await screen.findByTestId('wizard-header')
|
||||
const stepper = within(header).getByTestId('wizard-stepper')
|
||||
// Each step is a button — 7 buttons match WIZARD_STEPS.length.
|
||||
// Each step is a button — 8 buttons match WIZARD_STEPS.length
|
||||
// (Marketplace step inserted between Components and Domain — #710).
|
||||
const stepButtons = within(stepper).getAllByRole('button')
|
||||
expect(stepButtons).toHaveLength(WIZARD_STEPS.length)
|
||||
expect(WIZARD_STEPS.length).toBe(7)
|
||||
expect(WIZARD_STEPS.length).toBe(8)
|
||||
// Every step exposes a stable testid so visual regression tests can
|
||||
// pin to it without relying on text content.
|
||||
for (const step of WIZARD_STEPS) {
|
||||
|
||||
@ -10,7 +10,7 @@ import { ProfileMenu } from '@/widgets/auth/ProfileMenu'
|
||||
import { consumeProvisionFlashBanner } from '@/shared/lib/flashBanner'
|
||||
|
||||
/**
|
||||
* Wizard step list — seven progress stops in dependency order. StepSuccess
|
||||
* Wizard step list — eight progress stops in dependency order. StepSuccess
|
||||
* and StepNSDelegation are post-provisioning destinations after StepReview
|
||||
* launches provisioning; they are not part of the visible progress, so
|
||||
* they are not in this list.
|
||||
@ -27,14 +27,24 @@ import { consumeProvisionFlashBanner } from '@/shared/lib/flashBanner'
|
||||
* 4. Credentials — once each region has a provider, collect the API
|
||||
* token each chosen provider needs, plus the SSH key.
|
||||
* 5. Components — platform building-block selection.
|
||||
* 6. Domain — pool subdomain or BYO domain + admin email.
|
||||
* 7. Review — POST body preview + launch.
|
||||
* 6. Marketplace — opt into Marketplace mode (issue #710 wave 3a). The
|
||||
* toggle decides whether the Sovereign exposes a
|
||||
* per-tenant SaaS storefront at
|
||||
* marketplace.<sovereign-fqdn>.
|
||||
* 7. Domain — pool subdomain or BYO domain + admin email.
|
||||
* 8. Review — POST body preview + launch.
|
||||
*
|
||||
* Topology BEFORE Provider is the dependency-correct order: a provider is
|
||||
* a per-region property, picked AFTER topology decides how many regions
|
||||
* exist. SKU choices belong INSIDE the provider step because every cloud
|
||||
* has its own instance-type vocabulary (see shared/constants/providerSizes.ts).
|
||||
*
|
||||
* Marketplace is positioned AFTER Components and BEFORE Domain on purpose:
|
||||
* the operator has just curated the apps that will populate the storefront,
|
||||
* and the next step (Domain) names the Sovereign — making the marketplace
|
||||
* URL preview (`marketplace.<fqdn>`) meaningful in either order without a
|
||||
* forward-reference.
|
||||
*
|
||||
* After Review, the post-provisioning steps (StepSuccess, StepNSDelegation)
|
||||
* run on the same wizard state machine but live OUTSIDE the visible
|
||||
* progress indicator — they are operator-facing handover gates, not user
|
||||
@ -46,8 +56,9 @@ export const WIZARD_STEPS = [
|
||||
{ id: 3, label: 'Provider', desc: 'Cloud + region + sizing per slot' },
|
||||
{ id: 4, label: 'Credentials', desc: 'API tokens + SSH key' },
|
||||
{ id: 5, label: 'Components', desc: 'Platform building blocks' },
|
||||
{ id: 6, label: 'Domain', desc: 'Pool or BYO + admin email' },
|
||||
{ id: 7, label: 'Review', desc: 'Confirm and provision' },
|
||||
{ id: 6, label: 'Marketplace', desc: 'Multi-tenant SaaS storefront' },
|
||||
{ id: 7, label: 'Domain', desc: 'Pool or BYO + admin email' },
|
||||
{ id: 8, label: 'Review', desc: 'Confirm and provision' },
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@ -87,6 +87,24 @@ export interface SovereignPoolDomain {
|
||||
description: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Marketplace brand — purely cosmetic per-Sovereign theming for the
|
||||
* `marketplace.<sovereign-fqdn>` storefront when the operator opts into
|
||||
* Marketplace mode (issue #710 wave 3a). All three fields are optional;
|
||||
* the chart only consumes `marketplaceEnabled` today, but the brand
|
||||
* fields persist on the wizard state so a future settings page can
|
||||
* surface them without re-prompting on every wizard run. Values that are
|
||||
* empty strings mean "use the platform default" — never "unset".
|
||||
*/
|
||||
export interface MarketplaceBrand {
|
||||
/** Display name shown in the storefront header (e.g. "Acme Marketplace"). */
|
||||
name: string
|
||||
/** Short tagline rendered under the storefront name. */
|
||||
tagline: string
|
||||
/** CSS-style hex colour (e.g. "#38BDF8") used for the storefront accent. */
|
||||
primaryColor: string
|
||||
}
|
||||
|
||||
export interface WizardState {
|
||||
orgName: string; orgDomain: string; orgEmail: string; orgIndustry: string
|
||||
orgSize: string; orgHeadquarters: string; orgCompliance: string[]
|
||||
@ -211,6 +229,24 @@ export interface WizardState {
|
||||
selectedBlueprints: string[]
|
||||
regions: Region[]
|
||||
controlPlaneSize: NodeSize; workerSize: NodeSize; workerCount: number; haEnabled: boolean
|
||||
/**
|
||||
* Marketplace mode toggle (issue #710 wave 3a). When true, the
|
||||
* provisioner sets `marketplace_enabled=true` on the OpenTofu var that
|
||||
* cloud-init substitutes into the bp-catalyst-platform HelmRelease,
|
||||
* which in turn renders the marketplace HTTPRoutes (storefront +
|
||||
* per-tenant subdomain shells). The backend Request struct
|
||||
* (`provisioner.Request.MarketplaceEnabled`) and the chart values
|
||||
* (`ingress.marketplace.enabled`) are already wired — this flag is the
|
||||
* UI seam.
|
||||
*/
|
||||
marketplaceEnabled: boolean
|
||||
/**
|
||||
* Optional brand fields shown on `marketplace.<sovereign-fqdn>` when
|
||||
* Marketplace mode is on. Never required — empty strings mean "use the
|
||||
* platform default banner". Persisted in the wizard store so a return
|
||||
* visit (or a future settings page) doesn't lose them.
|
||||
*/
|
||||
marketplaceBrand: MarketplaceBrand
|
||||
/**
|
||||
* Selected platform components from the corporate StepComponents grid
|
||||
* (the 60+ catalog defined in pages/wizard/steps/componentGroups.ts).
|
||||
@ -434,6 +470,12 @@ export const INITIAL_WIZARD_STATE: WizardState = {
|
||||
// literal that won't pass validation against a non-hetzner provider.
|
||||
regions: [], controlPlaneSize: '', workerSize: '', workerCount: 0,
|
||||
haEnabled: false, selectedComponents: [...computeDefaultSelection()].sort(),
|
||||
// Marketplace mode (issue #710 wave 3a) — opt-in. Defaults off so a
|
||||
// first-run wizard provisions a private Sovereign; the operator can
|
||||
// flip the toggle on StepMarketplace before launch, and a future
|
||||
// settings page can flip it post-launch without touching the wizard.
|
||||
marketplaceEnabled: false,
|
||||
marketplaceBrand: { name: '', tagline: '', primaryColor: '' },
|
||||
airgap: false,
|
||||
currentStep: 1, completedSteps: [], deploymentId: null,
|
||||
lastProvisionResult: null,
|
||||
|
||||
@ -90,6 +90,42 @@ describe('wizard store — registrar credentials', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('wizard store — marketplace mode (#710 wave 3a)', () => {
|
||||
it('defaults marketplaceEnabled to false and brand to empty strings', () => {
|
||||
const s = useWizardStore.getState()
|
||||
expect(s.marketplaceEnabled).toBe(false)
|
||||
expect(s.marketplaceBrand).toEqual({ name: '', tagline: '', primaryColor: '' })
|
||||
})
|
||||
|
||||
it('setMarketplaceEnabled flips the flag without touching the brand', () => {
|
||||
useWizardStore.setState({
|
||||
...INITIAL_WIZARD_STATE,
|
||||
marketplaceBrand: { name: 'Acme', tagline: 'Apps for the regulated cloud', primaryColor: '#38BDF8' },
|
||||
})
|
||||
useWizardStore.getState().setMarketplaceEnabled(true)
|
||||
const s = useWizardStore.getState()
|
||||
expect(s.marketplaceEnabled).toBe(true)
|
||||
expect(s.marketplaceBrand).toEqual({
|
||||
name: 'Acme',
|
||||
tagline: 'Apps for the regulated cloud',
|
||||
primaryColor: '#38BDF8',
|
||||
})
|
||||
})
|
||||
|
||||
it('setMarketplaceBrand merges partial updates into the existing brand', () => {
|
||||
useWizardStore.setState({
|
||||
...INITIAL_WIZARD_STATE,
|
||||
marketplaceBrand: { name: 'Acme', tagline: 'old tagline', primaryColor: '#000000' },
|
||||
})
|
||||
useWizardStore.getState().setMarketplaceBrand({ tagline: 'new tagline' })
|
||||
expect(useWizardStore.getState().marketplaceBrand).toEqual({
|
||||
name: 'Acme',
|
||||
tagline: 'new tagline',
|
||||
primaryColor: '#000000',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('wizard store — persistence hygiene', () => {
|
||||
it('drops registrarToken and registrarTokenValidated from the persist payload', () => {
|
||||
useWizardStore.setState({
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
type NodeSize,
|
||||
type TopologyTemplate,
|
||||
type ProvisionResult,
|
||||
type MarketplaceBrand,
|
||||
} from './model'
|
||||
import {
|
||||
findComponent,
|
||||
@ -182,6 +183,13 @@ interface WizardActions {
|
||||
setWorkerSize: (size: NodeSize) => void
|
||||
setWorkerCount: (count: number) => void
|
||||
setHaEnabled: (enabled: boolean) => void
|
||||
|
||||
// Step — Marketplace (issue #710 wave 3a). Both setters are simple
|
||||
// mirrors of the setHaEnabled pattern; the brand setter takes a
|
||||
// partial so callers can edit one field at a time.
|
||||
setMarketplaceEnabled: (enabled: boolean) => void
|
||||
setMarketplaceBrand: (brand: Partial<MarketplaceBrand>) => void
|
||||
|
||||
toggleComponent: (component: SelectedComponent) => void
|
||||
setComponents: (components: SelectedComponent[]) => void
|
||||
}
|
||||
@ -491,6 +499,19 @@ export const useWizardStore = create<WizardStore>()(
|
||||
setWorkerSize: (workerSize) => set({ workerSize }, false, 'wizard/setWorkerSize'),
|
||||
setWorkerCount: (workerCount) => set({ workerCount }, false, 'wizard/setWorkerCount'),
|
||||
setHaEnabled: (haEnabled) => set({ haEnabled }, false, 'wizard/setHaEnabled'),
|
||||
|
||||
// Marketplace mode — issue #710 wave 3a. Toggle persists via the
|
||||
// existing deploy-request → tofu var → cloud-init → Flux substitute
|
||||
// → chart render path; no ConfigMap shortcut.
|
||||
setMarketplaceEnabled: (marketplaceEnabled) =>
|
||||
set({ marketplaceEnabled }, false, 'wizard/setMarketplaceEnabled'),
|
||||
setMarketplaceBrand: (brand) =>
|
||||
set(
|
||||
(s) => ({ marketplaceBrand: { ...s.marketplaceBrand, ...brand } }),
|
||||
false,
|
||||
'wizard/setMarketplaceBrand',
|
||||
),
|
||||
|
||||
toggleComponent: (component) =>
|
||||
// Legacy action — kept for back-compat with any old call site that
|
||||
// hands a SelectedComponent record. Internally we just toggle the
|
||||
@ -737,6 +758,24 @@ export const useWizardStore = create<WizardStore>()(
|
||||
if (p.lastProvisionResult === undefined) {
|
||||
p.lastProvisionResult = null
|
||||
}
|
||||
// Marketplace fields added in #710 wave 3a — coerce missing
|
||||
// values on a legacy persisted payload so StepMarketplace and
|
||||
// StepReview never crash on undefined.
|
||||
if (typeof p.marketplaceEnabled !== 'boolean') {
|
||||
p.marketplaceEnabled = false
|
||||
}
|
||||
if (
|
||||
!p.marketplaceBrand ||
|
||||
typeof p.marketplaceBrand !== 'object'
|
||||
) {
|
||||
p.marketplaceBrand = { name: '', tagline: '', primaryColor: '' }
|
||||
} else {
|
||||
p.marketplaceBrand = {
|
||||
name: typeof p.marketplaceBrand.name === 'string' ? p.marketplaceBrand.name : '',
|
||||
tagline: typeof p.marketplaceBrand.tagline === 'string' ? p.marketplaceBrand.tagline : '',
|
||||
primaryColor: typeof p.marketplaceBrand.primaryColor === 'string' ? p.marketplaceBrand.primaryColor : '',
|
||||
}
|
||||
}
|
||||
// SSH-key fields added after first install (#160) — coerce missing
|
||||
// values so the StepCredentials SSH section renders cleanly on a
|
||||
// legacy persisted payload.
|
||||
|
||||
@ -7,6 +7,7 @@ import { StepTopology } from './steps/StepTopology'
|
||||
import { StepProvider } from './steps/StepProvider'
|
||||
import { StepCredentials } from './steps/StepCredentials'
|
||||
import { StepComponents } from './steps/StepComponents'
|
||||
import { StepMarketplace } from './steps/StepMarketplace'
|
||||
import { StepReview } from './steps/StepReview'
|
||||
import { StepSuccess } from './steps/StepSuccess'
|
||||
import { StepNSDelegation } from './steps/StepNSDelegation'
|
||||
@ -24,10 +25,14 @@ import { StepNSDelegation } from './steps/StepNSDelegation'
|
||||
// why sizing lives here, not in topology.
|
||||
// 4. StepCredentials — API tokens (per chosen provider) + SSH key.
|
||||
// 5. StepComponents — unified marketplace catalog.
|
||||
// 6. StepDomain — pool subdomain or BYO domain + admin email.
|
||||
// 7. StepReview — single source of truth for the POST body.
|
||||
// 8. StepSuccess — provisioning result.
|
||||
// 9. StepNSDelegation — post-handover parent-zone NS delegation
|
||||
// 6. StepMarketplace — opt into Marketplace mode (issue #710 wave 3a).
|
||||
// The toggle decides whether the Sovereign exposes
|
||||
// a per-tenant SaaS storefront at
|
||||
// marketplace.<sovereign-fqdn>.
|
||||
// 7. StepDomain — pool subdomain or BYO domain + admin email.
|
||||
// 8. StepReview — single source of truth for the POST body.
|
||||
// 9. StepSuccess — provisioning result.
|
||||
// 10. StepNSDelegation — post-handover parent-zone NS delegation
|
||||
// (omantel.omani.works → Sovereign-owned PowerDNS).
|
||||
// Closes #374. Pure runbook-emitter by default;
|
||||
// "auto-apply" toggle gates a stub catalyst-api
|
||||
@ -38,6 +43,7 @@ const STEPS = [
|
||||
StepProvider,
|
||||
StepCredentials,
|
||||
StepComponents,
|
||||
StepMarketplace,
|
||||
StepDomain,
|
||||
StepReview,
|
||||
StepSuccess,
|
||||
|
||||
@ -0,0 +1,302 @@
|
||||
/**
|
||||
* StepMarketplace — Marketplace mode opt-in (issue #710 wave 3a).
|
||||
*
|
||||
* Inserted between StepComponents and StepDomain. The toggle decides
|
||||
* whether the operator wants to turn this Sovereign into a SaaS
|
||||
* platform (per-tenant subdomains, public storefront at
|
||||
* `marketplace.<sovereign-fqdn>`, isolated tenant shells) or keep it
|
||||
* as a private single-tenant install.
|
||||
*
|
||||
* Wiring (already in place — this step is the missing UI seam):
|
||||
*
|
||||
* StepMarketplace toggle → store.marketplaceEnabled
|
||||
* → StepReview POST /v1/deployments
|
||||
* { …, marketplaceEnabled: true }
|
||||
* → catalyst-api provisioner.Request.MarketplaceEnabled
|
||||
* → OpenTofu var marketplace_enabled
|
||||
* → cloud-init → Flux substitute
|
||||
* → bp-catalyst-platform values.yaml
|
||||
* ingress.marketplace.enabled = true
|
||||
* → marketplace HTTPRoutes rendered
|
||||
*
|
||||
* The brand fields (name / tagline / primary colour) are PURELY
|
||||
* cosmetic and persist on the wizard state so a future settings
|
||||
* page can read them without re-prompting. The chart only consumes
|
||||
* the enabled flag for now (companion PRs land settings + catalog
|
||||
* admin surfaces).
|
||||
*
|
||||
* Visual style mirrors StepComponents: StepShell wrapper, card grid
|
||||
* within `--wiz-bg-sub` surfaces, body fields in `--wiz-bg-input`,
|
||||
* inline help text in `--wiz-text-sub`. Footer Back/Next buttons are
|
||||
* published via useStepNav() — same contract every other step uses.
|
||||
*/
|
||||
|
||||
import { useWizardStore } from '@/entities/deployment/store'
|
||||
import { resolveSovereignDomain } from '@/entities/deployment/model'
|
||||
import { StepShell, useStepNav } from './_shared'
|
||||
|
||||
const FIELD_INPUT_STYLE: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '0.55rem 0.7rem',
|
||||
background: 'var(--wiz-bg-input)',
|
||||
border: '1px solid var(--wiz-border-sub)',
|
||||
borderRadius: 7,
|
||||
color: 'var(--wiz-text-hi)',
|
||||
font: 'inherit',
|
||||
fontSize: '0.88rem',
|
||||
}
|
||||
|
||||
const FIELD_LABEL_STYLE: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 600,
|
||||
color: 'var(--wiz-text-md)',
|
||||
letterSpacing: '0.02em',
|
||||
}
|
||||
|
||||
const FIELD_HELP_STYLE: React.CSSProperties = {
|
||||
fontSize: '0.74rem',
|
||||
fontWeight: 400,
|
||||
color: 'var(--wiz-text-sub)',
|
||||
lineHeight: 1.45,
|
||||
}
|
||||
|
||||
export function StepMarketplace() {
|
||||
const { next, back } = useStepNav()
|
||||
const store = useWizardStore()
|
||||
|
||||
const fqdn = resolveSovereignDomain(store)
|
||||
// Show a stable "marketplace.<fqdn>" preview when the user has filled
|
||||
// in the domain step ahead of this one; otherwise dim-fall back to a
|
||||
// generic placeholder so the operator still understands what the
|
||||
// toggle will produce.
|
||||
const marketplaceURL = fqdn ? `marketplace.${fqdn}` : 'marketplace.<your-sovereign-fqdn>'
|
||||
|
||||
const enabled = store.marketplaceEnabled
|
||||
const brand = store.marketplaceBrand
|
||||
|
||||
return (
|
||||
<StepShell
|
||||
title="Marketplace mode"
|
||||
description={
|
||||
'Marketplace mode turns this Sovereign into a multi-tenant SaaS platform. ' +
|
||||
'You publish apps from your unified catalog, customers sign up at the storefront, ' +
|
||||
'and each tenant runs in an isolated subdomain shell. Leave it off to provision a ' +
|
||||
'single-tenant private Sovereign — you can flip the toggle later from Settings.'
|
||||
}
|
||||
onNext={next}
|
||||
onBack={back}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
{/* ── Toggle card ──────────────────────────────────────── */}
|
||||
<div
|
||||
data-testid="step-marketplace-toggle-card"
|
||||
style={{
|
||||
border: `1.5px solid ${enabled ? '#4ADE80' : 'var(--wiz-border-sub)'}`,
|
||||
borderRadius: 12,
|
||||
padding: '0.95rem 1.1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '1rem',
|
||||
transition: 'border-color 0.15s, background 0.15s',
|
||||
background: enabled
|
||||
? 'color-mix(in srgb, #4ADE80 6%, var(--wiz-bg-sub))'
|
||||
: 'var(--wiz-bg-sub)',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.95rem',
|
||||
fontWeight: 600,
|
||||
color: 'var(--wiz-text-hi)',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Enable Marketplace mode
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
color: 'var(--wiz-text-md)',
|
||||
fontSize: '0.82rem',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Turn this Sovereign into a SaaS platform. Customers sign up at{' '}
|
||||
<code
|
||||
style={{
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
fontSize: '0.78rem',
|
||||
color: 'var(--wiz-accent)',
|
||||
background: 'rgba(56,189,248,0.08)',
|
||||
padding: '0.05rem 0.35rem',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{marketplaceURL}
|
||||
</code>
|
||||
, get isolated per-tenant subdomains, and consume apps you publish from your
|
||||
unified catalog.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pill switch — accessibility role="switch" plus the
|
||||
floating ball element. Clicking anywhere on the switch
|
||||
flips the store flag. */}
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
data-testid="step-marketplace-toggle"
|
||||
onClick={() => store.setMarketplaceEnabled(!enabled)}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 44,
|
||||
height: 24,
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
background: enabled ? '#4ADE80' : 'rgba(148,163,184,0.35)',
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
aria-label={enabled ? 'Disable Marketplace mode' : 'Enable Marketplace mode'}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
left: enabled ? 22 : 2,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: '#fff',
|
||||
transition: 'left 0.18s ease',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Brand fields (revealed when enabled) ─────────────── */}
|
||||
{enabled && (
|
||||
<div
|
||||
data-testid="step-marketplace-brand"
|
||||
style={{
|
||||
background: 'var(--wiz-bg-sub)',
|
||||
border: '1px solid var(--wiz-border-sub)',
|
||||
borderRadius: 12,
|
||||
padding: '1rem 1.1rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.85rem',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3
|
||||
style={{
|
||||
margin: '0 0 0.25rem',
|
||||
fontSize: '0.88rem',
|
||||
fontWeight: 600,
|
||||
color: 'var(--wiz-text-hi)',
|
||||
}}
|
||||
>
|
||||
Storefront branding
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: '0.78rem',
|
||||
color: 'var(--wiz-text-sub)',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Optional. All three fields can be left blank — the storefront falls back to
|
||||
platform defaults, and you can edit them later from the post-launch Settings
|
||||
page without re-running the wizard.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
|
||||
gap: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<label style={FIELD_LABEL_STYLE}>
|
||||
Brand name
|
||||
<input
|
||||
type="text"
|
||||
data-testid="step-marketplace-brand-name"
|
||||
placeholder="e.g. Acme Marketplace"
|
||||
value={brand.name}
|
||||
onChange={(e) => store.setMarketplaceBrand({ name: e.target.value })}
|
||||
style={FIELD_INPUT_STYLE}
|
||||
maxLength={64}
|
||||
/>
|
||||
<span style={FIELD_HELP_STYLE}>Shown in the storefront header.</span>
|
||||
</label>
|
||||
|
||||
<label style={FIELD_LABEL_STYLE}>
|
||||
Tagline
|
||||
<input
|
||||
type="text"
|
||||
data-testid="step-marketplace-brand-tagline"
|
||||
placeholder="e.g. Apps for the regulated cloud"
|
||||
value={brand.tagline}
|
||||
onChange={(e) => store.setMarketplaceBrand({ tagline: e.target.value })}
|
||||
style={FIELD_INPUT_STYLE}
|
||||
maxLength={120}
|
||||
/>
|
||||
<span style={FIELD_HELP_STYLE}>One-line subtitle below the brand name.</span>
|
||||
</label>
|
||||
|
||||
<label style={FIELD_LABEL_STYLE}>
|
||||
Primary colour
|
||||
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
data-testid="step-marketplace-brand-color"
|
||||
placeholder="#38BDF8"
|
||||
value={brand.primaryColor}
|
||||
onChange={(e) =>
|
||||
store.setMarketplaceBrand({ primaryColor: e.target.value })
|
||||
}
|
||||
style={{ ...FIELD_INPUT_STYLE, fontFamily: 'JetBrains Mono, monospace' }}
|
||||
maxLength={7}
|
||||
pattern="^#?[0-9A-Fa-f]{6}$"
|
||||
/>
|
||||
{/^#?[0-9A-Fa-f]{6}$/.test(brand.primaryColor) && (
|
||||
<span
|
||||
aria-hidden
|
||||
data-testid="step-marketplace-brand-color-swatch"
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
flexShrink: 0,
|
||||
borderRadius: 6,
|
||||
border: '1px solid var(--wiz-border-sub)',
|
||||
background: brand.primaryColor.startsWith('#')
|
||||
? brand.primaryColor
|
||||
: `#${brand.primaryColor}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span style={FIELD_HELP_STYLE}>
|
||||
CSS hex (e.g. <code>#38BDF8</code>). Used as the storefront accent.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StepShell>
|
||||
)
|
||||
}
|
||||
@ -667,6 +667,17 @@ export function StepReview() {
|
||||
objectStorageRegion: store.objectStorageRegion,
|
||||
objectStorageAccessKey: store.objectStorageAccessKey,
|
||||
objectStorageSecretKey: store.objectStorageSecretKey,
|
||||
// Marketplace mode (issue #710 wave 3a) — captured by
|
||||
// StepMarketplace. The catalyst-api Request struct already
|
||||
// accepts `marketplaceEnabled` and threads it through to
|
||||
// OpenTofu via var.marketplace_enabled (PR #719); from there
|
||||
// cloud-init substitutes it into the bp-catalyst-platform
|
||||
// HelmRelease values (`ingress.marketplace.enabled`). The
|
||||
// brand fields are captured for forward-compat with the
|
||||
// post-launch settings page (companion PR) and ignored by
|
||||
// the chart for now.
|
||||
marketplaceEnabled: store.marketplaceEnabled,
|
||||
marketplaceBrand: store.marketplaceBrand,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
@ -934,7 +945,110 @@ export function StepReview() {
|
||||
`}</style>
|
||||
</Section>
|
||||
|
||||
{/* ── 6. Domain ────────────────────────────────────────── */}
|
||||
{/* ── 6. Marketplace (issue #710 wave 3a) ──────────────── */}
|
||||
<Section
|
||||
title={
|
||||
<>
|
||||
<span>Marketplace</span>
|
||||
<span
|
||||
data-testid="review-marketplace-state"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.06em',
|
||||
color: store.marketplaceEnabled ? '#4ADE80' : 'var(--wiz-text-sub)',
|
||||
background: store.marketplaceEnabled
|
||||
? 'rgba(74,222,128,0.12)'
|
||||
: 'rgba(148,163,184,0.12)',
|
||||
border: store.marketplaceEnabled
|
||||
? '1px solid rgba(74,222,128,0.25)'
|
||||
: '1px solid var(--wiz-border-sub)',
|
||||
borderRadius: 3,
|
||||
padding: '1px 6px',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{store.marketplaceEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
testId="review-section-marketplace"
|
||||
>
|
||||
{store.marketplaceEnabled ? (
|
||||
<FieldGrid minColumnWidth={200}>
|
||||
<Field
|
||||
label="Storefront URL"
|
||||
fullWidth
|
||||
value={
|
||||
sovereignFQDN ? (
|
||||
<code
|
||||
style={{
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
fontSize: 11,
|
||||
color: '#38BDF8',
|
||||
}}
|
||||
>
|
||||
marketplace.{sovereignFQDN}
|
||||
</code>
|
||||
) : (
|
||||
<span style={{ color: 'var(--wiz-text-hint)' }}>
|
||||
— resolved from Domain step —
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
label="Brand name"
|
||||
value={dimIfMissing(store.marketplaceBrand.name, '— platform default —')}
|
||||
/>
|
||||
<Field
|
||||
label="Tagline"
|
||||
value={dimIfMissing(store.marketplaceBrand.tagline, '— platform default —')}
|
||||
/>
|
||||
<Field
|
||||
label="Primary colour"
|
||||
value={
|
||||
store.marketplaceBrand.primaryColor ? (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 3,
|
||||
border: '1px solid var(--wiz-border-sub)',
|
||||
background: store.marketplaceBrand.primaryColor.startsWith('#')
|
||||
? store.marketplaceBrand.primaryColor
|
||||
: `#${store.marketplaceBrand.primaryColor}`,
|
||||
}}
|
||||
/>
|
||||
<code style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11 }}>
|
||||
{store.marketplaceBrand.primaryColor}
|
||||
</code>
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: 'var(--wiz-text-hint)' }}>— platform default —</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FieldGrid>
|
||||
) : (
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 11,
|
||||
color: 'var(--wiz-text-sub)',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Single-tenant private Sovereign. No storefront, no per-tenant subdomains. You can
|
||||
flip Marketplace mode on later from the post-launch Settings page without
|
||||
re-provisioning.
|
||||
</p>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* ── 7. Domain ────────────────────────────────────────── */}
|
||||
<Section title="Domain" testId="review-section-domain">
|
||||
<FieldGrid minColumnWidth={200}>
|
||||
<Field label="Mode" value={DOMAIN_MODE_LABELS[store.sovereignDomainMode]} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user