Files
7838d00f57 feat: connector workbench (#641)
* wip: connector workbench

* wip: improve workbench

* feat(workbench): improve UI with sidebar, routing, and syntax highlighting

- Replace connector dropdown with Slack-style sidebar for connector selection
- Add URL-based routing with react-router-dom for browser navigation support
- Add JSON syntax highlighting with react-syntax-highlighter
- Rework State tab layout to list/detail view with filtering
- Update terminology from "create" to "instantiate" for connectors
- Fix duplicate selection bug in lists (ID generation collision)
- Unify data table row heights across tabs
- Increase code block font sizes for readability
- Hide navigation tabs on homepage when no connector selected
- Make CW logo clickable to return to home view

Co-authored-by: Cursor <[email protected]>

* fix(workbench): use textarea for PEM/certificate config fields

Fields containing 'privateKey', 'certificate', or 'pem' in their name
now use a textarea instead of single-line input, preserving newlines
in PEM content that would otherwise be stripped.

Co-authored-by: Cursor <[email protected]>

* fix(workbench): address PR review issues

- Fix data race on idCounter using atomic.Int64
- Fix path traversal vulnerability in introspect.go
- Fix pointer comparison for DefaultAsset in baseline.go

Co-authored-by: Cursor <[email protected]>

* feat(workbench): add generic connector server for integration testing

Expose the generic connector REST API from the workbench so staging
services can install a "generic connector" pointing to the local
workbench, enabling end-to-end integration testing.

- Add /generic/* routes (accounts, balances, beneficiaries, transactions)
- Return cached data from storage instead of making fresh API calls
- Add connector selection and optional API key authentication
- Add UI panel in Debug tab for configuration

Co-authored-by: Cursor <[email protected]>

* fix(workbench): use internal registry to avoid pkg dependency

Change imports from pkg/registry to internal/connectors/plugins/registry
so the workbench can be merged before the pkg refactoring. The internal
registry will automatically delegate to pkg/registry once that refactor
lands.

Also adds light/dark mode toggle to the UI with system preference detection.

Co-authored-by: Cursor <[email protected]>

* fix(workbench): address PR review feedback

- Add ctx.Done() checks in fetch loops to exit early on client disconnect
- Refactor generic server to get connector directly instead of via context,
  providing clearer data flow as suggested in review

Co-authored-by: Cursor <[email protected]>

* fix(workbench): include built UI files for embed directive

The //go:embed ui/dist/* directive requires files to exist at build time.
Add exception to .gitignore and commit the built UI distribution files.

Co-authored-by: Cursor <[email protected]>

* fix(workbench): address golangci-lint errors

- Remove unused globalDebugTransport variable
- Remove unused parseIntParam function
- Fix printf format for models.TaskType (use %v instead of %s)
- Add error return value handling for various function calls

Co-authored-by: Cursor <[email protected]>

* fix: remove .vite cache folder from git

Co-authored-by: Cursor <[email protected]>

* fix(workbench): fix remaining errcheck issue

Co-authored-by: Cursor <[email protected]>

* fix(workbench): fix all remaining lint issues

- Add error handling for Body.Close() calls
- Remove unnecessary fmt.Sprintf wrapper
- Simplify hex character check condition

Co-authored-by: Cursor <[email protected]>

* fix(workbench): fix errcheck in cmd/workbench.go

Co-authored-by: Cursor <[email protected]>

* chore: replace ui dist build artifacts with placeholder

The //go:embed directive needs at least one file to exist.
Instead of committing the entire built UI, commit only a
.gitkeep placeholder. The server has fallback code that
displays instructions when the full UI isn't available.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-02-12 18:29:17 +00:00

123 lines
4.2 KiB
Go

package workbench
import (
"encoding/json"
"time"
"github.com/formancehq/payments/internal/models"
)
// Response types with proper JSON tags for the workbench UI.
// These convert from internal models.PSP* types to API-friendly formats.
// AccountResponse is the JSON-serializable account for the UI.
type AccountResponse struct {
Reference string `json:"reference"`
CreatedAt time.Time `json:"created_at"`
Name *string `json:"name,omitempty"`
DefaultAsset *string `json:"default_asset,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Raw json.RawMessage `json:"raw,omitempty"`
}
// PaymentResponse is the JSON-serializable payment for the UI.
type PaymentResponse struct {
Reference string `json:"reference"`
ParentReference string `json:"parent_reference,omitempty"`
CreatedAt time.Time `json:"created_at"`
Type string `json:"type"`
Amount string `json:"amount"`
Asset string `json:"asset"`
Scheme string `json:"scheme,omitempty"`
Status string `json:"status"`
SourceAccountReference *string `json:"source_account_reference,omitempty"`
DestinationAccountReference *string `json:"destination_account_reference,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Raw json.RawMessage `json:"raw,omitempty"`
}
// BalanceResponse is the JSON-serializable balance for the UI.
type BalanceResponse struct {
AccountReference string `json:"account_reference"`
CreatedAt time.Time `json:"created_at"`
Amount string `json:"amount"`
Asset string `json:"asset"`
}
// ToAccountResponse converts a PSPAccount to an AccountResponse.
func ToAccountResponse(acc models.PSPAccount) AccountResponse {
return AccountResponse{
Reference: acc.Reference,
CreatedAt: acc.CreatedAt,
Name: acc.Name,
DefaultAsset: acc.DefaultAsset,
Metadata: acc.Metadata,
Raw: acc.Raw,
}
}
// ToAccountResponses converts a slice of PSPAccounts to AccountResponses.
func ToAccountResponses(accounts []models.PSPAccount) []AccountResponse {
result := make([]AccountResponse, len(accounts))
for i, acc := range accounts {
result[i] = ToAccountResponse(acc)
}
return result
}
// ToPaymentResponse converts a PSPPayment to a PaymentResponse.
func ToPaymentResponse(pay models.PSPPayment) PaymentResponse {
amount := "0"
if pay.Amount != nil {
amount = pay.Amount.String()
}
return PaymentResponse{
Reference: pay.Reference,
ParentReference: pay.ParentReference,
CreatedAt: pay.CreatedAt,
Type: pay.Type.String(),
Amount: amount,
Asset: pay.Asset,
Scheme: pay.Scheme.String(),
Status: pay.Status.String(),
SourceAccountReference: pay.SourceAccountReference,
DestinationAccountReference: pay.DestinationAccountReference,
Metadata: pay.Metadata,
Raw: pay.Raw,
}
}
// ToPaymentResponses converts a slice of PSPPayments to PaymentResponses.
func ToPaymentResponses(payments []models.PSPPayment) []PaymentResponse {
result := make([]PaymentResponse, len(payments))
for i, pay := range payments {
result[i] = ToPaymentResponse(pay)
}
return result
}
// ToBalanceResponse converts a PSPBalance to a BalanceResponse.
func ToBalanceResponse(bal models.PSPBalance) BalanceResponse {
amount := "0"
if bal.Amount != nil {
amount = bal.Amount.String()
}
return BalanceResponse{
AccountReference: bal.AccountReference,
CreatedAt: bal.CreatedAt,
Amount: amount,
Asset: bal.Asset,
}
}
// ToBalanceResponses converts a slice of PSPBalances to BalanceResponses.
func ToBalanceResponses(balances []models.PSPBalance) []BalanceResponse {
result := make([]BalanceResponse, len(balances))
for i, bal := range balances {
result[i] = ToBalanceResponse(bal)
}
return result
}