Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions backend/internal/api/handlers/pagerduty_migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@ import (

// pdClientFactory is a package-level variable so tests can override it to point
// the real PagerDuty client at a mock HTTP server.
var pdClientFactory = func(apiKey string) *pagerduty.Client {
return pagerduty.NewClient(apiKey)
var pdClientFactory = func(apiKey, baseURL string) *pagerduty.Client {
return pagerduty.NewClientWithBaseURL(apiKey, baseURL)
}

// pdBaseURL maps a region string ("us" or "eu") to the PagerDuty API base URL.
// Defaults to the US endpoint for empty or unrecognised values.
func pdBaseURL(region string) string {
if region == "eu" {
return "https://api.eu.pagerduty.com"
}
return "https://api.pagerduty.com"
}

type pdMigrationRequest struct {
APIKey string `json:"api_key" binding:"required"`
Region string `json:"region"`
Force bool `json:"force"`
}

Expand Down Expand Up @@ -57,7 +67,7 @@ func PreviewPagerDutyMigration(
return
}

client := pdClientFactory(req.APIKey)
client := pdClientFactory(req.APIKey, pdBaseURL(req.Region))
if err := client.ValidateAPIKey(); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"message": err.Error()}})
return
Expand Down Expand Up @@ -87,7 +97,7 @@ func ImportPagerDutyMigration(
return
}

client := pdClientFactory(req.APIKey)
client := pdClientFactory(req.APIKey, pdBaseURL(req.Region))
if err := client.ValidateAPIKey(); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"message": err.Error()}})
return
Expand Down
35 changes: 34 additions & 1 deletion backend/internal/api/handlers/pagerduty_migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func newPDMockServer(t *testing.T, cfg pdMockConfig) *httptest.Server {
func overridePDFactory(t *testing.T, baseURL string) {
t.Helper()
original := pdClientFactory
pdClientFactory = func(apiKey string) *pagerduty.Client {
pdClientFactory = func(apiKey, _ string) *pagerduty.Client {
return pagerduty.NewClientWithBaseURL(apiKey, baseURL)
}
t.Cleanup(func() { pdClientFactory = original })
Expand Down Expand Up @@ -278,3 +278,36 @@ func TestImportPagerDutyMigration_InvalidAPIKey(t *testing.T) {
w := postJSON(r, "/api/v1/migrations/pagerduty/import", map[string]string{"api_key": "bad"})
assert.Equal(t, http.StatusBadRequest, w.Code)
}

// ── Region tests ──────────────────────────────────────────────────────────────

func TestPDBaseURL_DefaultsToUS(t *testing.T) {
assert.Equal(t, "https://api.pagerduty.com", pdBaseURL(""))
assert.Equal(t, "https://api.pagerduty.com", pdBaseURL("us"))
}

func TestPDBaseURL_EU(t *testing.T) {
assert.Equal(t, "https://api.eu.pagerduty.com", pdBaseURL("eu"))
}

func TestPreviewPagerDutyMigration_EURegion(t *testing.T) {
srv := newPDMockServer(t, pdMockConfig{})
overridePDFactory(t, srv.URL)

db := setupMigrationTestDB(t)
r := buildPDRouter(repository.NewScheduleRepository(db), repository.NewEscalationPolicyRepository(db))

w := postJSON(r, "/api/v1/migrations/pagerduty/preview", map[string]string{"api_key": "tok", "region": "eu"})
assert.Equal(t, http.StatusOK, w.Code)
}

func TestImportPagerDutyMigration_EURegion(t *testing.T) {
srv := newPDMockServer(t, pdMockConfig{})
overridePDFactory(t, srv.URL)

db := setupMigrationTestDB(t)
r := buildPDRouter(repository.NewScheduleRepository(db), repository.NewEscalationPolicyRepository(db))

w := postJSON(r, "/api/v1/migrations/pagerduty/import", map[string]string{"api_key": "tok", "region": "eu"})
assert.Equal(t, http.StatusOK, w.Code)
}
1 change: 1 addition & 0 deletions frontend/src/api/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export async function importOnCallMigration(

export interface PDMigrationRequest {
api_key: string
region?: 'us' | 'eu'
force?: boolean
}

Expand Down
34 changes: 31 additions & 3 deletions frontend/src/components/migrations/PagerDutyImportPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '../../api/migrations'

type Step = 'idle' | 'previewing' | 'preview' | 'importing' | 'done'
type Region = 'us' | 'eu'

interface PagerDutyImportPanelProps {
onComplete?: () => void
Expand All @@ -22,6 +23,7 @@ function extractError(e: unknown): string {
export function PagerDutyImportPanel({ onComplete }: PagerDutyImportPanelProps) {
const [step, setStep] = useState<Step>('idle')
const [apiKey, setApiKey] = useState('')
const [region, setRegion] = useState<Region>('us')
const [showKey, setShowKey] = useState(false)
const [force, setForce] = useState(false)
const [error, setError] = useState('')
Expand All @@ -39,7 +41,7 @@ export function PagerDutyImportPanel({ onComplete }: PagerDutyImportPanelProps)
}
setStep('previewing')
try {
const data = await previewPagerDutyMigration({ api_key: apiKey.trim() })
const data = await previewPagerDutyMigration({ api_key: apiKey.trim(), region })
setPreview(data)
setStep('preview')
} catch (e) {
Expand All @@ -53,7 +55,7 @@ export function PagerDutyImportPanel({ onComplete }: PagerDutyImportPanelProps)
setError('')
setStep('importing')
try {
const data = await importPagerDutyMigration({ api_key: apiKey.trim(), force })
const data = await importPagerDutyMigration({ api_key: apiKey.trim(), region, force })
setResult(data)
setStep('done')
} catch (e) {
Expand All @@ -72,9 +74,35 @@ export function PagerDutyImportPanel({ onComplete }: PagerDutyImportPanelProps)
</div>
)}

{/* ── Step 1: API key input ─────────────────────────────────────────── */}
{/* ── Step 1: API key + region input ───────────────────────────────── */}
{(step === 'idle' || step === 'previewing') && (
<div className="space-y-4">
{/* Region selector */}
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
Region
</label>
<div className="flex gap-4">
{(['us', 'eu'] as Region[]).map((r) => (
<label key={r} className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="pd-region"
value={r}
checked={region === r}
onChange={() => setRegion(r)}
className="accent-brand-primary"
/>
<span className="text-sm text-text-primary uppercase">{r}</span>
<span className="text-xs text-text-secondary">
({r === 'us' ? 'api.pagerduty.com' : 'api.eu.pagerduty.com'})
</span>
</label>
))}
</div>
</div>

{/* API key */}
<div>
<label className="block text-sm font-medium text-text-primary mb-1">
PagerDuty API Key
Expand Down
Loading