From 1b2bb72fe9c671cbed28de1a66dd12671b5bb7f4 Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Wed, 26 Aug 2026 16:08:30 +0300 Subject: [PATCH] refactor: rename Integration to Subscription across the UI --- SW.Bitween.Api/Domain/Accounts/Role.cs | 2 +- SW.Bitween.Sdk/Model/Permissions.cs | 22 +- .../ClientApp/e2e/exchanges.spec.ts | 4 +- SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts | 36 +-- .../e2e/permissions-enforcement.spec.ts | 4 +- ...grations.spec.ts => subscriptions.spec.ts} | 6 +- .../ClientApp/e2e/view-guards.spec.ts | 8 +- SW.Bitween.Web/ClientApp/src/api/client.ts | 100 ++++----- .../ClientApp/src/api/http/dashboard.ts | 26 +-- .../ClientApp/src/api/http/documents.ts | 12 +- .../ClientApp/src/api/http/exchanges.ts | 26 +-- .../ClientApp/src/api/http/gateways.ts | 40 ++-- .../ClientApp/src/api/http/globalValues.ts | 14 +- .../ClientApp/src/api/http/httpClient.ts | 4 +- .../ClientApp/src/api/http/notifiers.ts | 8 +- .../ClientApp/src/api/http/retryPolicies.ts | 36 +-- .../src/api/http/subscriptionBody.ts | 14 +- .../{integrations.ts => subscriptions.ts} | 118 +++++----- .../ClientApp/src/api/http/workGroups.ts | 12 +- .../ClientApp/src/api/permissions.ts | 2 +- SW.Bitween.Web/ClientApp/src/api/types.ts | 128 +++++------ .../src/components/config/AdapterConfig.tsx | 2 +- .../config/InformationTypeFields.tsx | 2 +- .../src/components/config/PartnerFields.tsx | 10 +- ...ationDialog.tsx => SubscriptionDialog.tsx} | 28 +-- .../src/components/config/WorkGroupDialog.tsx | 6 +- .../src/components/config/pickers.tsx | 30 +-- .../src/components/config/shared.tsx | 150 ++++++------- .../mapper/useMappingEditorLoader.ts | 4 +- .../src/components/mapper/useSave.ts | 4 +- .../ClientApp/src/components/ui/BackLink.tsx | 2 +- .../ClientApp/src/lib/busMessageName.ts | 2 +- SW.Bitween.Web/ClientApp/src/nav.ts | 7 +- .../src/pages/api-gateways/ApiGatewayPage.tsx | 18 +- .../pages/api-gateways/ApiGatewaysPage.tsx | 18 +- .../pages/api-gateways/AttachPartnerPage.tsx | 32 +-- .../pages/api-gateways/EditAttachmentPage.tsx | 24 +- ...age.tsx => NewGatewaySubscriptionPage.tsx} | 56 ++--- .../src/pages/bus-gateways/BusGatewayPage.tsx | 208 +++++++++--------- .../pages/bus-gateways/BusGatewaysPage.tsx | 20 +- .../src/pages/bus-gateways/studio/Canvas.tsx | 80 +++---- .../pages/bus-gateways/studio/Inspector.tsx | 74 +++---- .../pages/bus-gateways/studio/RouteList.tsx | 22 +- .../src/pages/bus-gateways/studio/model.ts | 70 +++--- .../src/pages/dashboard/DashboardPage.tsx | 22 +- .../src/pages/exchanges/ExchangeNewPage.tsx | 34 +-- .../src/pages/exchanges/ExchangesPage.tsx | 28 +-- .../ClientApp/src/pages/exchanges/shared.tsx | 2 +- .../ClientApp/src/pages/flow/FlowMap.tsx | 2 +- .../ClientApp/src/pages/flow/FlowPage.tsx | 10 +- .../ClientApp/src/pages/flow/layout.ts | 2 +- .../ClientApp/src/pages/flow/model.ts | 36 +-- .../global-values/GlobalValueSetPage.tsx | 28 +-- .../global-values/GlobalValueSetsPage.tsx | 8 +- .../information-types/InformationTypePage.tsx | 4 +- .../InformationTypesPage.tsx | 8 +- .../src/pages/notifiers/NotifierPage.tsx | 40 ++-- .../src/pages/notifiers/NotifiersPage.tsx | 14 +- .../src/pages/partners/PartnerPage.tsx | 12 +- .../src/pages/partners/PartnersPage.tsx | 6 +- .../pages/queue-health/QueueHealthPage.tsx | 10 +- .../src/pages/retry-policies/AlertRouting.tsx | 2 +- .../src/pages/retry-policies/GroupDialog.tsx | 4 +- .../retry-policies/RetryPoliciesPage.tsx | 8 +- .../pages/retry-policies/RetryPolicyPage.tsx | 10 +- .../src/pages/retry-policies/UsagePanel.tsx | 58 ++--- .../scheduled-jobs/NewScheduledJobPage.tsx | 28 +-- .../scheduled-jobs/ScheduledJobsPage.tsx | 34 +-- .../ScheduledRetriesPage.tsx | 38 ++-- .../SubscriptionPage.tsx} | 98 ++++----- .../SubscriptionsPage.tsx} | 42 ++-- .../studio/Overview.tsx | 26 +-- .../studio/ReceiveAttemptsPanel.tsx | 6 +- .../studio/ResponseFields.tsx | 28 +-- .../studio/RetryBudget.tsx | 24 +- .../studio/StageRail.tsx | 2 +- .../studio/faces.ts | 20 +- .../studio/model.ts | 28 +-- .../studio/stages.ts | 8 +- .../src/pages/work-groups/WorkGroupPage.tsx | 6 +- .../src/pages/work-groups/WorkGroupsPage.tsx | 12 +- SW.Bitween.Web/ClientApp/src/router.tsx | 14 +- 82 files changed, 1125 insertions(+), 1128 deletions(-) rename SW.Bitween.Web/ClientApp/e2e/{integrations.spec.ts => subscriptions.spec.ts} (92%) rename SW.Bitween.Web/ClientApp/src/api/http/{integrations.ts => subscriptions.ts} (84%) rename SW.Bitween.Web/ClientApp/src/components/config/{IntegrationDialog.tsx => SubscriptionDialog.tsx} (84%) rename SW.Bitween.Web/ClientApp/src/pages/api-gateways/{NewGatewayIntegrationPage.tsx => NewGatewaySubscriptionPage.tsx} (85%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations/IntegrationPage.tsx => subscriptions/SubscriptionPage.tsx} (83%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations/IntegrationsPage.tsx => subscriptions/SubscriptionsPage.tsx} (89%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations => subscriptions}/studio/Overview.tsx (93%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations => subscriptions}/studio/ReceiveAttemptsPanel.tsx (95%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations => subscriptions}/studio/ResponseFields.tsx (90%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations => subscriptions}/studio/RetryBudget.tsx (79%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations => subscriptions}/studio/StageRail.tsx (98%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations => subscriptions}/studio/faces.ts (89%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations => subscriptions}/studio/model.ts (86%) rename SW.Bitween.Web/ClientApp/src/pages/{integrations => subscriptions}/studio/stages.ts (89%) diff --git a/SW.Bitween.Api/Domain/Accounts/Role.cs b/SW.Bitween.Api/Domain/Accounts/Role.cs index 99db343e..08c6193e 100644 --- a/SW.Bitween.Api/Domain/Accounts/Role.cs +++ b/SW.Bitween.Api/Domain/Accounts/Role.cs @@ -17,7 +17,7 @@ public class Role : BaseEntity, IAudited public const int ViewerId = 3; /// The groups the built-in Member and Viewer roles reach; Administration stays admin-only. - private static readonly string[] NonAdminGroups = ["Operate", "Integrations", "Configuration"]; + private static readonly string[] NonAdminGroups = ["Operate", "Subscriptions", "Configuration"]; private Role() { diff --git a/SW.Bitween.Sdk/Model/Permissions.cs b/SW.Bitween.Sdk/Model/Permissions.cs index 083d1bc2..2a31d394 100644 --- a/SW.Bitween.Sdk/Model/Permissions.cs +++ b/SW.Bitween.Sdk/Model/Permissions.cs @@ -178,46 +178,46 @@ private static PermissionAreaModel Area(string id, string label, string group, s Area("dashboard", "Dashboard", "Operate", "Traffic and health overview (reached from the logo).", (View, "See the dashboard.")), - // ——— Integrations ——— - Area("subscriptions", "Integrations", "Integrations", "The configured pipelines that process exchanges.", - (View, "Browse integrations and their configuration."), - (Create, "Create integrations."), + // ——— Subscriptions ——— + Area("subscriptions", "Subscriptions", "Subscriptions", "The configured pipelines that process exchanges.", + (View, "Browse subscriptions and their configuration."), + (Create, "Create subscriptions."), (Edit, "Change adapters, mappings and settings."), - (Delete, "Delete integrations."), + (Delete, "Delete subscriptions."), (Operate, "Pause, resume, receive now, aggregate now.")), - Area("partners", "Partners", "Integrations", "The external parties you exchange data with.", + Area("partners", "Partners", "Subscriptions", "The external parties you exchange data with.", (View, "Browse partners and their properties."), (Create, "Create partners."), (Edit, "Change partner details, properties and API keys."), (Delete, "Delete partners.")), - Area("documents", "Information types", "Integrations", + Area("documents", "Information types", "Subscriptions", "The kinds of business documents that flow between partners.", (View, "Browse information types."), (Create, "Create information types."), (Edit, "Change information types, codes and promoted properties."), (Delete, "Delete unused information types.")), - Area("global-values", "Global values", "Integrations", "Shared value sets adapters can reference.", + Area("global-values", "Global values", "Subscriptions", "Shared value sets adapters can reference.", (View, "Browse global value sets."), (Create, "Create value sets."), (Edit, "Change value sets."), (Delete, "Delete value sets.")), - Area("notifiers", "Notifiers", "Integrations", "Alerts sent when exchanges fail or succeed.", + Area("notifiers", "Notifiers", "Subscriptions", "Alerts sent when exchanges fail or succeed.", (View, "Browse notifiers and their delivery history."), (Create, "Create notifiers."), (Edit, "Change notifiers."), (Delete, "Remove notifiers.")), - Area("api-gateways", "API gateways", "Integrations", "HTTP entry points partners call into.", + Area("api-gateways", "API gateways", "Subscriptions", "HTTP entry points partners call into.", (View, "Browse API gateways and attached partners."), (Create, "Create new API gateways."), (Edit, "Change gateways and partner attachments."), (Delete, "Delete API gateways.")), - Area("bus-gateways", "Bus gateways", "Integrations", "Bus listeners that route documents to integrations.", + Area("bus-gateways", "Bus gateways", "Subscriptions", "Bus listeners that route documents to subscriptions.", (View, "Browse bus gateways and routes."), (Create, "Create new bus gateways."), (Edit, "Change gateways and routes."), diff --git a/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts b/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts index 7ba60137..21299294 100644 --- a/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/exchanges.spec.ts @@ -44,9 +44,9 @@ test("exchanges list, filter, retry, bulk retry, create", async ({ page }) => { await page.getByRole("dialog").getByRole("button", { name: "Retry" }).click(); await expect(page.getByRole("dialog")).toHaveCount(0, { timeout: 15000 }); - // Manually create an exchange addressed at an integration. + // Manually create an exchange addressed at a subscription. await page.goto("exchanges/new"); - await page.getByRole("combobox", { name: "Pick an integration…" }).click(); + await page.getByRole("combobox", { name: "Pick a subscription…" }).click(); await page.getByRole("option", { name: "s3 test sub" }).click(); // Dismiss the dropdown panel via an outside click (it sits above the panel's // anchor point, so it can't itself be covered) rather than Escape, which diff --git a/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts b/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts index da8358d6..12b9617b 100644 --- a/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/gateways.spec.ts @@ -11,7 +11,7 @@ test.beforeEach(async ({ page }) => { await page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15000 }); }); -test("API gateway: create, attach partner, create integration detour, edit attachment, detach, delete", async ({ +test("API gateway: create, attach partner, create subscription detour, edit attachment, detach, delete", async ({ page, }) => { const name = `Playwright API GW ${Date.now()}`; @@ -22,38 +22,38 @@ test("API gateway: create, attach partner, create integration detour, edit attac await expect(page).toHaveURL(/\/api-gateways\/\d+$/); await expect(page.getByRole("heading", { name })).toBeVisible(); - // Attach a partner, detouring to create the required GatewayApiCall integration inline. + // Attach a partner, detouring to create the required GatewayApiCall subscription inline. await page.getByRole("button", { name: "Attach partner" }).click(); await expect(page).toHaveURL(/\/api-gateways\/\d+\/attach$/); await page.getByRole("button", { name: "acme" }).click(); await page.getByRole("button", { name: "Continue" }).click(); - const integrationName = `Playwright GW Integration ${Date.now()}`; - await page.getByRole("link", { name: "New integration" }).click(); + const subscriptionName = `Playwright GW Subscription ${Date.now()}`; + await page.getByRole("link", { name: "New subscription" }).click(); await expect(page).toHaveURL(/\/subscriptions\/new\?type=GatewayApiCall/); - await page.fill("#ni-name", integrationName); + await page.fill("#ni-name", subscriptionName); await page.getByRole("button", { name: "test doc" }).click(); await page.getByLabel("handler adapter").click(); await page.getByRole("option", { name: "NativeHttpHandler" }).click(); await expect(page.getByRole("listbox")).toHaveCount(0, { timeout: 10000 }); await page.locator("#prop-Url").fill("https://example.com/sink"); - await page.getByRole("button", { name: "Create integration" }).click(); + await page.getByRole("button", { name: "Create subscription" }).click(); // This page itself renders a ReturnBanner with a "Continue" button before // the mutation resolves (inherited from the detour link) — wait for the - // create to actually land on the new integration's own page first, or the + // create to actually land on the new subscription's own page first, or the // click races and hits that stale button instead. await expect(page).toHaveURL(/\/subscriptions\/\d+\?/); await page.getByRole("button", { name: "Continue" }).click(); await expect(page).toHaveURL(/\/api-gateways\/\d+\/attach$/); - await expect(page.getByText(integrationName)).toBeVisible(); + await expect(page.getByText(subscriptionName)).toBeVisible(); await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Attach partner" }).click(); await expect(page).toHaveURL(/\/api-gateways\/\d+$/); await expect(page.getByText("acme").first()).toBeVisible(); - await expect(page.getByText(integrationName)).toBeVisible(); + await expect(page.getByText(subscriptionName)).toBeVisible(); // Edit the attachment — exercises the remove-then-add path (backend's // updatepartner can't mutate a composite-key column in place). @@ -61,7 +61,7 @@ test("API gateway: create, attach partner, create integration detour, edit attac await expect(page).toHaveURL(/\/api-gateways\/\d+\/attachments\/\d+$/); await page.getByRole("button", { name: "Save" }).click(); await expect(page).toHaveURL(/\/api-gateways\/\d+$/); - await expect(page.getByText(integrationName)).toBeVisible(); + await expect(page.getByText(subscriptionName)).toBeVisible(); // Detach. await page.getByRole("button", { name: "Detach acme" }).click(); @@ -80,7 +80,7 @@ test("API gateway: create, attach partner, create integration detour, edit attac .getByRole("button", { name: "Delete gateway" }) .click(); // ApiGatewayPage navigates to /api-gateways, which the router redirects to - // the unified integrations list. + // the unified subscriptions list. await expect(page).toHaveURL(/\/subscriptions\?types=api-gateways$/); }); @@ -103,28 +103,28 @@ test("Bus gateway: create, add route with match expression, edit route, remove, await page.getByRole("button", { name: "No partner" }).click(); await page.getByRole("button", { name: "Continue" }).click(); - // Integration step — detour to create the required BusGateway integration. - const integrationName = `Playwright Bus Integration ${Date.now()}`; - await page.getByRole("link", { name: "New integration" }).click(); + // Subscription step — detour to create the required BusGateway subscription. + const subscriptionName = `Playwright Bus Subscription ${Date.now()}`; + await page.getByRole("link", { name: "New subscription" }).click(); await expect(page).toHaveURL(/\/subscriptions\/new\?type=BusGateway/); - await page.fill("#ni-name", integrationName); + await page.fill("#ni-name", subscriptionName); await page.getByLabel("handler adapter").click(); await page.getByRole("option", { name: "NativeHttpHandler" }).click(); await expect(page.getByRole("listbox")).toHaveCount(0, { timeout: 10000 }); await page.locator("#prop-Url").fill("https://example.com/sink"); - await page.getByRole("button", { name: "Create integration" }).click(); + await page.getByRole("button", { name: "Create subscription" }).click(); // Wait for the create to actually land (see the comment in the API gateway // test above) before clicking the ReturnBanner's "Continue". await expect(page).toHaveURL(/\/subscriptions\/\d+\?/); await page.getByRole("button", { name: "Continue" }).click(); await expect(page).toHaveURL(/\/bus-gateways\/\d+\/add-route$/); - await expect(page.getByText(integrationName)).toBeVisible(); + await expect(page.getByText(subscriptionName)).toBeVisible(); await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Add route" }).click(); await expect(page).toHaveURL(/\/bus-gateways\/\d+$/); - await expect(page.getByText(integrationName)).toBeVisible(); + await expect(page.getByText(subscriptionName)).toBeVisible(); // Edit the route (no-op save exercises the round trip of a null match expression). await page.getByRole("button", { name: /Edit route \d+/ }).click(); diff --git a/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts b/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts index b2e29b85..675cb5eb 100644 --- a/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/permissions-enforcement.spec.ts @@ -51,7 +51,7 @@ test("a custom role grants exactly what was ticked, in the nav, by URL, and at t // 1. The sidebar offers the one page they can see, and nothing else. await expect(sidebarLinks(page).filter({ hasText: "Exchanges" })).toBeVisible(); - for (const hidden of ["Partners", "Integrations", "Work groups", "Team", "Settings"]) + for (const hidden of ["Partners", "Subscriptions", "Work groups", "Team", "Settings"]) await expect(sidebarLinks(page).filter({ hasText: hidden })).toHaveCount(0); // 2. Typing the URL of a page they lack doesn't get them in. @@ -109,7 +109,7 @@ test("Viewer can read but not write", async ({ page }) => { await removeMember(page, email); }); -test("Member can configure integrations but not manage the team", async ({ page }) => { +test("Member can configure subscriptions but not manage the team", async ({ page }) => { await signInAsAdmin(page); const email = await addMember(page, { name: "Regular Member", roles: ["Member"] }); diff --git a/SW.Bitween.Web/ClientApp/e2e/integrations.spec.ts b/SW.Bitween.Web/ClientApp/e2e/subscriptions.spec.ts similarity index 92% rename from SW.Bitween.Web/ClientApp/e2e/integrations.spec.ts rename to SW.Bitween.Web/ClientApp/e2e/subscriptions.spec.ts index d680e71a..26c1818b 100644 --- a/SW.Bitween.Web/ClientApp/e2e/integrations.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/subscriptions.spec.ts @@ -44,12 +44,12 @@ test("scheduled job create, adapters, pause/resume, receive now, list, delete", // Pause / resume. await page.getByRole("button", { name: "Pause" }).click(); - await page.getByRole("dialog", { name: "Pause this integration?" }).getByRole("button", { name: "Pause" }).click(); + await page.getByRole("dialog", { name: "Pause this subscription?" }).getByRole("button", { name: "Pause" }).click(); await expect(page.getByRole("dialog")).toHaveCount(0); await expect(page.getByText("Paused", { exact: true }).first()).toBeVisible(); await page.getByRole("button", { name: "Resume" }).click(); - await page.getByRole("dialog", { name: "Resume this integration?" }).getByRole("button", { name: "Resume" }).click(); + await page.getByRole("dialog", { name: "Resume this subscription?" }).getByRole("button", { name: "Resume" }).click(); await expect(page.getByRole("dialog")).toHaveCount(0); await expect(page.getByText("Paused", { exact: true })).toHaveCount(0); @@ -73,7 +73,7 @@ test("scheduled job create, adapters, pause/resume, receive now, list, delete", await row.getByRole("button", { name: `Open ${name}` }).click(); await expect(page).toHaveURL(/\/subscriptions\/\d+$/); await page.getByRole("button", { name: "Delete" }).click(); - await page.getByRole("button", { name: "Delete integration" }).click(); + await page.getByRole("button", { name: "Delete subscription" }).click(); await expect(page).toHaveURL(/\/subscriptions$/); await expect(page.getByText(name)).toHaveCount(0); }); diff --git a/SW.Bitween.Web/ClientApp/e2e/view-guards.spec.ts b/SW.Bitween.Web/ClientApp/e2e/view-guards.spec.ts index be9133a7..7d339a18 100644 --- a/SW.Bitween.Web/ClientApp/e2e/view-guards.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/view-guards.spec.ts @@ -91,19 +91,19 @@ test("lookup mode stays readable, because pickers across the app depend on it", }); test("a page still loads when the area behind its Used by count is refused", async ({ page }) => { - const roleName = `PW No Integrations ${Date.now()}`; + const roleName = `PW No Subscriptions ${Date.now()}`; await signInAsAdmin(page); await createRole(page, { name: roleName, permissions: [{ area: "Information types", action: "View" }], }); - const email = await addMember(page, { name: "No Integrations", roles: [roleName] }); + const email = await addMember(page, { name: "No Subscriptions", roles: [roleName] }); await signOut(page); await signIn(page, email, FIRST_PASSWORD); - // The information types list counts how many integrations use each type, which needs the - // integrations list this role can't read. The count is what's expendable, not the page. + // The information types list counts how many subscriptions use each type, which needs the + // subscriptions list this role can't read. The count is what's expendable, not the page. await page.goto("information-types"); await expect(page.getByText("You don't have access to this page")).toHaveCount(0); await expect(page.getByRole("table")).toBeVisible(); diff --git a/SW.Bitween.Web/ClientApp/src/api/client.ts b/SW.Bitween.Web/ClientApp/src/api/client.ts index c861fbae..21f919fe 100644 --- a/SW.Bitween.Web/ClientApp/src/api/client.ts +++ b/SW.Bitween.Web/ClientApp/src/api/client.ts @@ -17,13 +17,13 @@ import type { InformationType, InformationTypeDetail, InformationTypeRow, - Integration, - IntegrationDetail, - IntegrationInfo, - IntegrationLastRun, - IntegrationRow, - IntegrationRun, - IntegrationType, + Subscription, + SubscriptionDetail, + SubscriptionInfo, + SubscriptionLastRun, + SubscriptionRow, + SubscriptionRun, + SubscriptionType, MatchGroup, Notifier, NotifierDetail, @@ -157,24 +157,24 @@ export interface ApiClient { ): Promise; deleteValueSet(id: string): Promise; - // — integrations (light summaries; cache aggressively) — - listIntegrations(): Promise; + // — subscriptions (light summaries; cache aggressively) — + listSubscriptions(): Promise; - // — integrations — - listIntegrationRows(): Promise; - searchIntegrationRows(query: { + // — subscriptions — + listSubscriptionRows(): Promise; + searchSubscriptionRows(query: { search: string; - type: IntegrationType | null; + type: SubscriptionType | null; informationTypeId?: number | null; partnerId?: number | null; inactive?: boolean | null; offset: number; limit: number; - }): Promise>; - getIntegration(id: number): Promise; - /** One call, one transaction: the integration exists as asked for, or not at all. */ - createIntegration(input: { - type: IntegrationType; + }): Promise>; + getSubscription(id: number): Promise; + /** One call, one transaction: the subscription exists as asked for, or not at all. */ + createSubscription(input: { + type: SubscriptionType; name: string; informationTypeId: number; /** Required by the types that carry their own partner — Internal and ApiCall. */ @@ -189,15 +189,15 @@ export interface ApiClient { handlerProperties?: Record; schedules?: Schedule[]; retryPolicyId?: number | null; - responseIntegrationId?: number | null; + responseSubscriptionId?: number | null; responseMessageTypeName?: string | null; enabled?: boolean; - }): Promise; - updateIntegration( + }): Promise; + updateSubscription( id: number, changes: Partial< Pick< - Integration, + Subscription, | "name" | "enabled" | "workGroupId" @@ -212,24 +212,24 @@ export interface ApiClient { | "handlerProperties" | "matchExpression" | "schedules" - | "responseIntegrationId" + | "responseSubscriptionId" | "responseMessageTypeName" > >, - ): Promise; - deleteIntegration(id: number): Promise; - /** Toggles paused: paused integrations accept work but hold it. */ - pauseIntegration(id: number): Promise; - receiveNow(id: number): Promise; - /** Run history for one scheduled integration, newest first. Empty for unscheduled types. */ - listIntegrationRuns(id: number, limit?: number): Promise; + ): Promise; + deleteSubscription(id: number): Promise; + /** Toggles paused: paused subscriptions accept work but hold it. */ + pauseSubscription(id: number): Promise; + receiveNow(id: number): Promise; + /** Run history for one scheduled subscription, newest first. Empty for unscheduled types. */ + listSubscriptionRuns(id: number, limit?: number): Promise; searchReceiveAttempts( subscriptionId: number, query: { outcome: ReceiveOutcome | null; offset: number; limit: number }, ): Promise>; - /** Newest run of every scheduled integration — one request for a whole list. */ - listLastRuns(): Promise; - /** Will these schedules actually fire? Asks the scheduler, not the integration record. */ + /** Newest run of every scheduled subscription — one request for a whole list. */ + listLastRuns(): Promise; + /** Will these schedules actually fire? Asks the scheduler, not the subscription record. */ listScheduleHealth(): Promise; listAdapters(kind: AdapterKind): Promise; @@ -263,9 +263,9 @@ export interface ApiClient { changes: { name: string; urlName: string; inactive: boolean }, ): Promise; deleteApiGateway(id: number): Promise; - /** The integration is either an existing id or defined inline; the endpoint commits both as one. */ + /** The subscription is either an existing id or defined inline; the endpoint commits both as one. */ attachGatewayPartner(id: number, input: AttachPartnerInput): Promise; - updateGatewayAttachment(id: number, input: { partnerId: number; integrationId: number }): Promise; + updateGatewayAttachment(id: number, input: { partnerId: number; subscriptionId: number }): Promise; removeGatewayAttachment(id: number, partnerId: number): Promise; // — bus gateways — @@ -281,12 +281,12 @@ export interface ApiClient { createBusGateway(input: { name: string; informationTypeId: number }): Promise; updateBusGateway(id: number, changes: { name: string; inactive: boolean }): Promise; deleteBusGateway(id: number): Promise; - /** The integration is either an existing id or defined inline; the endpoint commits both as one. */ + /** The subscription is either an existing id or defined inline; the endpoint commits both as one. */ addBusRoute(id: number, input: AddBusRouteInput): Promise; updateBusRoute( id: number, routeId: number, - input: { integrationId: number; partnerId: number | null; matchExpression: MatchGroup | null }, + input: { subscriptionId: number; partnerId: number | null; matchExpression: MatchGroup | null }, ): Promise; removeBusRoute(id: number, routeId: number): Promise; @@ -317,24 +317,24 @@ export interface ApiClient { attempts: number; }): Promise; - /** Spent budget and alert routing for every integration-and-group pair under this policy. */ + /** Spent budget and alert routing for every subscription-and-group pair under this policy. */ getRetryUsage(policyId: number): Promise; /** - * The same report for one integration, which is the only way to reach one whose policy is an + * The same report for one subscription, which is the only way to reach one whose policy is an * inline `CustomRetryPolicy` — those carry no policy id for the policy-scoped report to address, * yet still spend budget and can sit stopped with no counter anyone can see. */ - getIntegrationRetryUsage(integrationId: number): Promise; - /** The failures one group caught for one integration — what its spent budget went on. */ - getRetryAttempts(policyId: number, pair: { integrationId: number; groupId: string }): Promise; + getSubscriptionRetryUsage(subscriptionId: number): Promise; + /** The failures one group caught for one subscription — what its spent budget went on. */ + getRetryAttempts(policyId: number, pair: { subscriptionId: number; groupId: string }): Promise; /** Hands a spent budget back so the group retries again. Omit a field to reset across it. */ - resetRetryUsage(policyId: number, pair?: { integrationId?: number; groupId?: string }): Promise; - /** Reset by integration, for the inline-policy case the policy-scoped reset cannot reach. */ - resetIntegrationRetryUsage(integrationId: number, groupId?: string): Promise; + resetRetryUsage(policyId: number, pair?: { subscriptionId?: number; groupId?: string }): Promise; + /** Reset by subscription, for the inline-policy case the policy-scoped reset cannot reach. */ + resetSubscriptionRetryUsage(subscriptionId: number, groupId?: string): Promise; /** Sets, changes or clears where one pair's alert goes — the most specific level. */ saveRetryAlertOverride( policyId: number, - input: { integrationId: number; groupId: string } & RetryAlertConfig, + input: { subscriptionId: number; groupId: string } & RetryAlertConfig, ): Promise; // — settings — @@ -357,17 +357,17 @@ export interface ApiClient { getExchangeDocument(key: string): Promise; /** * Re-runs an exchange from its input file. `reset` re-resolves adapter - * properties from the integration's current configuration instead of the + * properties from the subscription's current configuration instead of the * values captured when the exchange first ran. Fails with * AUTO_RETRY_SCHEDULED when an auto-retry is already pending. */ retryExchange(id: string, opts: { reset: boolean }): Promise<{ id: string }>; /** Retries many; exchanges with a pending auto-retry are skipped, not failed. */ bulkRetryExchanges(ids: string[], opts: { reset: boolean }): Promise<{ retried: number; skipped: number }>; - /** Manually injects a payload, addressed at an integration or an information type. */ + /** Manually injects a payload, addressed at a subscription or an information type. */ createExchange(input: { - target: "integration" | "informationType"; - integrationId?: number; + target: "subscription" | "informationType"; + subscriptionId?: number; informationTypeId?: number; data: string; }): Promise<{ id: string }>; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts index 7a7279c1..426e2305 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/dashboard.ts @@ -1,6 +1,6 @@ import type { ApiClient } from "../client"; import type { DashboardData, ExchangeStatus } from "../types"; -import { integrationMethods } from "./integrations"; +import { subscriptionMethods } from "./subscriptions"; import { get } from "./request"; // ——— backend shapes (camelCase over the wire) ——— @@ -40,18 +40,18 @@ export const dashboardMethods = { // request, and exact rather than approximated. Bounded to a generous page // size for what's a modest-scale ops tool; a very high-volume deployment // would need real pagination here. - const [xchangeRes, delayedRes, alertsRaw, integrationRows] = await Promise.all([ + const [xchangeRes, delayedRes, alertsRaw, subscriptionRows] = await Promise.all([ get>( `/xchanges?filter=${encodeURIComponent(`StartedOn:6:${new Date(windowStart).toISOString()}`)}&size=1000&sort=StartedOn:1`, ), get>("/delayedretries?size=1"), get("/ops/alerts"), - integrationMethods.listIntegrationRows(), + subscriptionMethods.listSubscriptionRows(), ]); const rows = xchangeRes.result; const startedAt = (r: RawXchangeForDashboard) => Date.parse(r.startedOn); - const integrationNameById = new Map(integrationRows.map((i) => [i.id, i.name])); + const subscriptionNameById = new Map(subscriptionRows.map((i) => [i.id, i.name])); const todayRows = rows.filter((r) => startedAt(r) >= startOfTodayUtc); const yesterdayRows = rows.filter((r) => { @@ -81,16 +81,16 @@ export const dashboardMethods = { }; }); - const byIntegration = new Map(); + const bySubscription = new Map(); for (const r of week) { if (r.subscriptionId === null) continue; - const entry = byIntegration.get(r.subscriptionId) ?? { count: 0, failed: 0 }; + const entry = bySubscription.get(r.subscriptionId) ?? { count: 0, failed: 0 }; entry.count++; if (isBad(r)) entry.failed++; - byIntegration.set(r.subscriptionId, entry); + bySubscription.set(r.subscriptionId, entry); } - const busiest = [...byIntegration.entries()] - .map(([id, v]) => ({ id, name: integrationNameById.get(id) ?? `#${id}`, ...v })) + const busiest = [...bySubscription.entries()] + .map(([id, v]) => ({ id, name: subscriptionNameById.get(id) ?? `#${id}`, ...v })) .sort((a, b) => b.count - a.count) .slice(0, 5); @@ -101,8 +101,8 @@ export const dashboardMethods = { .map((r) => ({ id: r.id, status: toStatus(r), - integrationId: r.subscriptionId, - integrationName: r.subscriptionId !== null ? (integrationNameById.get(r.subscriptionId) ?? null) : null, + subscriptionId: r.subscriptionId, + subscriptionName: r.subscriptionId !== null ? (subscriptionNameById.get(r.subscriptionId) ?? null) : null, informationTypeCode: r.documentName, on: r.startedOn, exception: r.exception, @@ -122,10 +122,10 @@ export const dashboardMethods = { busiest, latestFailures, attention: { - failingIntegrations: integrationRows + failingSubscriptions: subscriptionRows .filter((i) => i.consecutiveFailures > 0) .map((i) => ({ id: i.id, name: i.name, consecutiveFailures: i.consecutiveFailures })), - pausedIntegrations: integrationRows.filter((i) => i.paused).map((i) => ({ id: i.id, name: i.name })), + pausedSubscriptions: subscriptionRows.filter((i) => i.paused).map((i) => ({ id: i.id, name: i.name })), }, }; }, diff --git a/SW.Bitween.Web/ClientApp/src/api/http/documents.ts b/SW.Bitween.Web/ClientApp/src/api/http/documents.ts index 9e1652ee..0bed1fd0 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/documents.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/documents.ts @@ -4,7 +4,7 @@ import type { InformationTypeDetail, InformationTypeFormat, InformationTypeRow, - IntegrationType, + SubscriptionType, Paged, TrailEntry, } from "../types"; @@ -43,7 +43,7 @@ interface RawTrailEntry { createdBy: string; } -const SUB_TYPE_BY_NUM: Record = { +const SUB_TYPE_BY_NUM: Record = { 1: "Internal", 2: "ApiCall", 4: "Receiving", @@ -51,7 +51,7 @@ const SUB_TYPE_BY_NUM: Record = { 16: "GatewayApiCall", 32: "BusGateway", }; -const INTEGRATION_TYPES: IntegrationType[] = [ +const SUBSCRIPTION_TYPES: SubscriptionType[] = [ "Receiving", "GatewayApiCall", "BusGateway", @@ -60,9 +60,9 @@ const INTEGRATION_TYPES: IntegrationType[] = [ "Aggregation", ]; /** Enums may arrive as the numeric value or the name in any case. */ -const toIntegrationType = (t: number | string): IntegrationType => { +const toSubscriptionType = (t: number | string): SubscriptionType => { if (typeof t === "number") return SUB_TYPE_BY_NUM[t] ?? "Internal"; - return INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; + return SUBSCRIPTION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; }; async function fetchSubscriptionsByDocument(documentId: number): Promise { @@ -108,7 +108,7 @@ async function fetchDetail(id: number): Promise { ]); return { ...toInformationType(d), - integrationSetups: subs.map((s) => ({ id: s.id, name: s.name, type: toIntegrationType(s.type) })), + subscriptionSetups: subs.map((s) => ({ id: s.id, name: s.name, type: toSubscriptionType(s.type) })), busGateways: busGateways .filter((g) => g.informationTypeId === id) .map((g) => ({ gatewayId: g.id, gatewayName: g.name })), diff --git a/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts b/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts index 643236f1..7cf934b4 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/exchanges.ts @@ -73,8 +73,8 @@ const STATUS_FILTER: Record = { const toExchangeRow = (raw: RawXchangeRow, partnerNameById: Map): ExchangeRow => ({ id: raw.id, status: deriveStatus(raw), - integrationId: raw.subscriptionId, - integrationName: raw.subscriptionName, + subscriptionId: raw.subscriptionId, + subscriptionName: raw.subscriptionName, informationTypeId: raw.documentId, informationTypeCode: raw.documentName, partnerId: raw.partnerId, @@ -103,8 +103,8 @@ const toExchangeRow = (raw: RawXchangeRow, partnerNameById: Map) const toScheduledRetryRow = (raw: RawDelayedRetryRow): ScheduledRetryRow => ({ id: raw.id, on: raw.on, - integrationId: raw.subscriptionId, - integrationName: raw.subscriptionName, + subscriptionId: raw.subscriptionId, + subscriptionName: raw.subscriptionName, informationTypeId: raw.documentId, informationTypeCode: raw.documentName, exception: raw.exception, @@ -126,7 +126,7 @@ const toScheduledRetryRow = (raw: RawDelayedRetryRow): ScheduledRetryRow => ({ function buildExchangeQuery(query: ExchangeQuery): string { const params = new URLSearchParams(); if (query.status) params.append("filter", `StatusFilter:1:${STATUS_FILTER[query.status]}`); - if (query.integrationId !== undefined) params.append("filter", `SubscriptionId:1:${query.integrationId}`); + if (query.subscriptionId !== undefined) params.append("filter", `SubscriptionId:1:${query.subscriptionId}`); if (query.partnerId !== undefined) params.append("filter", `PartnerId:1:${query.partnerId}`); if (query.informationTypeId !== undefined) params.append("filter", `DocumentId:1:${query.informationTypeId}`); if (query.ids?.trim()) { @@ -150,7 +150,7 @@ function buildExchangeQuery(query: ExchangeQuery): string { function buildScheduledRetryQuery(query: ScheduledRetryQuery): string { const params = new URLSearchParams(); - if (query.integrationId !== undefined) params.append("filter", `SubscriptionId:1:${query.integrationId}`); + if (query.subscriptionId !== undefined) params.append("filter", `SubscriptionId:1:${query.subscriptionId}`); if (query.informationTypeId !== undefined) params.append("filter", `DocumentId:1:${query.informationTypeId}`); if (query.exception?.trim()) params.append("filter", `Exception:4:${query.exception.trim()}`); if (query.from) params.append("filter", `On:6:${query.from}`); @@ -206,23 +206,23 @@ export const exchangeMethods = { }, async createExchange(input: { - target: "integration" | "informationType"; - integrationId?: number; + target: "subscription" | "informationType"; + subscriptionId?: number; informationTypeId?: number; data: string; }): Promise<{ id: string }> { const filter = - input.target === "integration" - ? `SubscriptionId:1:${input.integrationId}` + input.target === "subscription" + ? `SubscriptionId:1:${input.subscriptionId}` : `DocumentId:1:${input.informationTypeId}`; await post("/xchanges", { - option: input.target === "integration" ? "SubscriberId" : "DocumentId", - subscriberId: input.target === "integration" ? input.integrationId : null, + option: input.target === "subscription" ? "SubscriberId" : "DocumentId", + subscriberId: input.target === "subscription" ? input.subscriptionId : null, documentId: input.target === "informationType" ? input.informationTypeId : null, data: input.data, }); // Create.cs returns null too — look up the exchange it just created. When - // addressed at an information type, every matching integration gets its + // addressed at an information type, every matching subscription gets its // own exchange; we can only link to one, so take the newest. const res = await get>( `/xchanges?filter=${encodeURIComponent(filter)}&sort=StartedOn:2&size=1`, diff --git a/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts b/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts index ef76bcdd..ffff5ece 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/gateways.ts @@ -8,12 +8,12 @@ import type { BusGatewayDetail, BusGatewayRoute, BusGatewayRow, - InlineIntegrationDraft, + InlineSubscriptionDraft, MatchGroup, Paged, } from "../types"; import { toMatchGroup, toRawMatchExpression, type RawMatchSpec } from "./matchExpression"; -import { inlineIntegrationBody } from "./subscriptionBody"; +import { inlineSubscriptionBody } from "./subscriptionBody"; import { get, post, request } from "./request"; import { buildListQuery, SEARCHY_RULE } from "./searchQuery"; @@ -59,8 +59,8 @@ interface RawBusGateway { const toApiGatewayAttachment = (p: RawApiGatewayPartner): ApiGatewayAttachment => ({ partnerId: p.partnerId, partnerName: p.partnerName, - integrationId: p.subscriptionId, - integrationName: p.subscriptionName, + subscriptionId: p.subscriptionId, + subscriptionName: p.subscriptionName, }); const toApiGatewayRow = (raw: RawApiGateway): ApiGatewayRow => ({ @@ -84,8 +84,8 @@ const toApiGatewayDetail = (raw: RawApiGateway): ApiGatewayDetail => ({ const toBusGatewayRoute = (r: RawBusGatewayRoute): BusGatewayRoute => ({ id: r.id, - integrationId: r.subscriptionId, - integrationName: r.subscriptionName ?? "", + subscriptionId: r.subscriptionId, + subscriptionName: r.subscriptionName ?? "", partnerId: r.partnerId, partnerName: r.partnerName, matchExpression: toMatchGroup(r.matchExpression), @@ -113,20 +113,20 @@ const toBusGatewayDetail = (raw: RawBusGateway): BusGatewayDetail => ({ routes: (raw.routes ?? []).map(toBusGatewayRoute), }); -/** The attachment always points at an integration that already exists — a new one is +/** The attachment always points at a subscription that already exists — a new one is * created on its own page first, not inline here (unlike a bus gateway route, which * still creates one in the same transaction — see `AddBusRouteInput`). */ -export type AttachPartnerInput = { partnerId: number; integrationId: number }; +export type AttachPartnerInput = { partnerId: number; subscriptionId: number }; /** - * A route points at an integration that already exists, or defines one. Exactly one, + * A route points at a subscription that already exists, or defines one. Exactly one, * which the endpoint enforces — the union makes that unrepresentable rather than * merely wrong. */ export type AddBusRouteInput = { partnerId: number | null; matchExpression: MatchGroup | null; -} & ({ integrationId: number } | { newIntegration: InlineIntegrationDraft }); +} & ({ subscriptionId: number } | { newSubscription: InlineSubscriptionDraft }); export const gatewayMethods = { // ——— API gateways ——— @@ -193,17 +193,17 @@ export const gatewayMethods = { async attachGatewayPartner(id: number, input: AttachPartnerInput): Promise { await post(`/apigateways/${id}/addpartner`, { partnerId: input.partnerId, - subscriptionId: input.integrationId, + subscriptionId: input.subscriptionId, }); }, - async updateGatewayAttachment(id: number, input: { partnerId: number; integrationId: number }): Promise { + async updateGatewayAttachment(id: number, input: { partnerId: number; subscriptionId: number }): Promise { // Not a plain POST to updatepartner: ApiGatewayPartner's PK is the composite // (gatewayId, partnerId, subscriptionId), and the backend's UpdatePartner // handler tries to mutate subscriptionId in place on a tracked entity — EF // Core rejects changes to a key column. Remove-then-add sidesteps it. await post(`/apigateways/${id}/removepartner`, { partnerId: input.partnerId }); - await post(`/apigateways/${id}/addpartner`, { partnerId: input.partnerId, subscriptionId: input.integrationId }); + await post(`/apigateways/${id}/addpartner`, { partnerId: input.partnerId, subscriptionId: input.subscriptionId }); }, async removeGatewayAttachment(id: number, partnerId: number): Promise { @@ -284,11 +284,13 @@ export const gatewayMethods = { async addBusRoute(id: number, input: AddBusRouteInput): Promise { await post(`/busgateways/${id}/addroute`, { - // Exactly one of the two, which is what the endpoint enforces. An integration + // Exactly one of the two, which is what the endpoint enforces. A subscription // defined here is created in the same transaction as the route. - ...("newIntegration" in input - ? { newIntegration: inlineIntegrationBody(input.newIntegration) } - : { subscriptionId: input.integrationId }), + // `newIntegration` is the wire name: BusGatewayRouteCreate.NewIntegration still + // carries the old wording, so the key sent here cannot follow this UI's rename. + ...("newSubscription" in input + ? { newIntegration: inlineSubscriptionBody(input.newSubscription) } + : { subscriptionId: input.subscriptionId }), partnerId: input.partnerId, matchExpression: toRawMatchExpression(input.matchExpression), }); @@ -297,11 +299,11 @@ export const gatewayMethods = { async updateBusRoute( id: number, routeId: number, - input: { integrationId: number; partnerId: number | null; matchExpression: MatchGroup | null }, + input: { subscriptionId: number; partnerId: number | null; matchExpression: MatchGroup | null }, ): Promise { await post(`/busgateways/${id}/updateroute`, { routeId, - subscriptionId: input.integrationId, + subscriptionId: input.subscriptionId, partnerId: input.partnerId, matchExpression: toRawMatchExpression(input.matchExpression), }); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/globalValues.ts b/SW.Bitween.Web/ClientApp/src/api/http/globalValues.ts index 38ec544c..51268ee4 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/globalValues.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/globalValues.ts @@ -1,5 +1,5 @@ import type { ApiClient } from "../client"; -import type { GlobalValuesSet, GlobalValuesSetDetail, GlobalValuesSetRow, IntegrationType, ValueSetUsage } from "../types"; +import type { GlobalValuesSet, GlobalValuesSetDetail, GlobalValuesSetRow, SubscriptionType, ValueSetUsage } from "../types"; import { referencesGlobal, scanReferenceTokens } from "./references"; import { get, getEnrichment, post } from "./request"; @@ -26,7 +26,7 @@ interface RawSubscriptionForUsage { validatorProperties: RawKeyAndValue[] | null; } -const SUB_TYPE_BY_NUM: Record = { +const SUB_TYPE_BY_NUM: Record = { 1: "Internal", 2: "ApiCall", 4: "Receiving", @@ -34,7 +34,7 @@ const SUB_TYPE_BY_NUM: Record = { 16: "GatewayApiCall", 32: "BusGateway", }; -const INTEGRATION_TYPES: IntegrationType[] = [ +const SUBSCRIPTION_TYPES: SubscriptionType[] = [ "Receiving", "GatewayApiCall", "BusGateway", @@ -43,9 +43,9 @@ const INTEGRATION_TYPES: IntegrationType[] = [ "Aggregation", ]; /** Enums may arrive as the numeric value or the name in any case. */ -const toIntegrationType = (t: number | string): IntegrationType => { +const toSubscriptionType = (t: number | string): SubscriptionType => { if (typeof t === "number") return SUB_TYPE_BY_NUM[t] ?? "Internal"; - return INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; + return SUBSCRIPTION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; }; // GlobalAdapterValuesSet has no CreatedOn column on the backend. @@ -72,7 +72,7 @@ function globalKeysReferencedBy(sub: RawSubscriptionForUsage, setId: string): st export const globalValuesMethods = { - // No usage scan here: the list page answers "used by" from the integrations + // No usage scan here: the list page answers "used by" from the subscriptions // cache it already holds, so a second full /subscriptions fetch would be waste. async listValueSets(): Promise { const res = await get>("/globaladaptervaluessets"); @@ -86,7 +86,7 @@ export const globalValuesMethods = { ]); const usedBy: ValueSetUsage[] = subs .map((s) => ({ - integrationSetup: { id: s.id, name: s.name, type: toIntegrationType(s.type) }, + subscriptionSetup: { id: s.id, name: s.name, type: toSubscriptionType(s.type) }, keys: globalKeysReferencedBy(s, id), })) .filter((u) => u.keys.length > 0); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts b/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts index 9b3dd3e7..8efb95e5 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/httpClient.ts @@ -6,7 +6,7 @@ import { documentMethods } from "./documents"; import { exchangeMethods } from "./exchanges"; import { gatewayMethods } from "./gateways"; import { globalValuesMethods } from "./globalValues"; -import { integrationMethods } from "./integrations"; +import { subscriptionMethods } from "./subscriptions"; import { mapperMethods } from "./mappers"; import { notifierMethods } from "./notifiers"; import { partnerMethods } from "./partners"; @@ -30,7 +30,7 @@ const wired: Partial = { ...globalValuesMethods, ...workGroupMethods, ...retryPolicyMethods, - ...integrationMethods, + ...subscriptionMethods, ...adapterMethods, ...gatewayMethods, ...exchangeMethods, diff --git a/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts b/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts index b55a54fb..91e749db 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/notifiers.ts @@ -56,7 +56,7 @@ const toNotifier = (r: RawNotifier): Notifier => ({ onSuccess: r.runOnSuccessfulResult, channelId: r.handlerId ?? "", channelProperties: toRecord(r.handlerProperties), - integrationIds: (r.runOnSubscriptions ?? []).map((s) => s.id), + subscriptionIds: (r.runOnSubscriptions ?? []).map((s) => s.id), createdOn: "", }); @@ -98,7 +98,7 @@ export const notifierMethods = { onSuccess: !!r.runOnSuccessfulResult, channelId: r.handlerId ?? "", channelProperties: {}, - integrationIds: r.runOnSubscriptions ?? [], + subscriptionIds: r.runOnSubscriptions ?? [], createdOn: "", })), }; @@ -117,7 +117,7 @@ export const notifierMethods = { onSuccess: false, channelId: "", channelProperties: {}, - integrationIds: [], + subscriptionIds: [], createdOn: "", }; }, @@ -131,7 +131,7 @@ export const notifierMethods = { handlerId: changes.channelId, inactive: !changes.enabled, handlerProperties: toKvArray(changes.channelProperties), - runOnSubscriptions: changes.integrationIds.map((subscriptionId) => ({ id: subscriptionId })), + runOnSubscriptions: changes.subscriptionIds.map((subscriptionId) => ({ id: subscriptionId })), }); return { id, createdOn: "", ...changes }; }, diff --git a/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts b/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts index ade1a22e..9d488724 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/retryPolicies.ts @@ -1,7 +1,7 @@ import type { ApiClient } from "../client"; import { ApiRequestError, - type IntegrationType, + type SubscriptionType, type RetryAlertConfig, type RetryAlertLevel, type RetryAttempts, @@ -39,7 +39,7 @@ interface RawSubscriptionRef { type: number | string; } -const SUB_TYPE_BY_NUM: Record = { +const SUB_TYPE_BY_NUM: Record = { 1: "Internal", 2: "ApiCall", 4: "Receiving", @@ -47,7 +47,7 @@ const SUB_TYPE_BY_NUM: Record = { 16: "GatewayApiCall", 32: "BusGateway", }; -const INTEGRATION_TYPES: IntegrationType[] = [ +const SUBSCRIPTION_TYPES: SubscriptionType[] = [ "Receiving", "GatewayApiCall", "BusGateway", @@ -56,9 +56,9 @@ const INTEGRATION_TYPES: IntegrationType[] = [ "Aggregation", ]; /** Enums may arrive as the numeric value or the name in any case. */ -const toIntegrationType = (t: number | string): IntegrationType => { +const toSubscriptionType = (t: number | string): SubscriptionType => { if (typeof t === "number") return SUB_TYPE_BY_NUM[t] ?? "Internal"; - return INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; + return SUBSCRIPTION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; }; async function fetchSubscriptionsByRetryPolicy(retryPolicyId: number): Promise { @@ -167,7 +167,7 @@ async function fetchDetail(id: number): Promise { createdOn: "", alertHandlerId: r.alertHandlerId ?? null, alertHandlerProperties: r.alertHandlerProperties ?? {}, - integrations: subs.map((s) => ({ id: s.id, name: s.name, type: toIntegrationType(s.type) })), + subscriptions: subs.map((s) => ({ id: s.id, name: s.name, type: toSubscriptionType(s.type) })), }; } @@ -193,8 +193,8 @@ interface RawUsageRow { } const toUsageRow = (r: RawUsageRow): RetryUsageRow => ({ - integrationId: r.subscriptionId, - integrationName: r.subscriptionName, + subscriptionId: r.subscriptionId, + subscriptionName: r.subscriptionName, groupId: r.groupId, groupName: r.groupName, used: r.attemptsUsed, @@ -310,18 +310,18 @@ export const retryPolicyMethods = { return (rows ?? []).map(toUsageRow); }, - async getIntegrationRetryUsage(integrationId: number): Promise { - const rows = await post(`/subscriptions/${integrationId}/retryusage`, {}); + async getSubscriptionRetryUsage(subscriptionId: number): Promise { + const rows = await post(`/subscriptions/${subscriptionId}/retryusage`, {}); return (rows ?? []).map(toUsageRow); }, async getRetryAttempts( policyId: number, - pair: { integrationId: number; groupId: string }, + pair: { subscriptionId: number; groupId: string }, ): Promise { const res = await post<{ total: number; attempts: RawAttempt[] }>( `/retrypolicies/${policyId}/attempts`, - { subscriptionId: pair.integrationId, groupId: pair.groupId }, + { subscriptionId: pair.subscriptionId, groupId: pair.groupId }, ); return { total: res?.total ?? 0, @@ -336,23 +336,23 @@ export const retryPolicyMethods = { }; }, - async resetRetryUsage(policyId: number, pair?: { integrationId?: number; groupId?: string }): Promise { + async resetRetryUsage(policyId: number, pair?: { subscriptionId?: number; groupId?: string }): Promise { await post(`/retrypolicies/${policyId}/resetusage`, { - subscriptionId: pair?.integrationId ?? null, + subscriptionId: pair?.subscriptionId ?? null, groupId: pair?.groupId ?? null, }); }, - async resetIntegrationRetryUsage(integrationId: number, groupId?: string): Promise { - await post(`/subscriptions/${integrationId}/resetretryusage`, { groupId: groupId ?? null }); + async resetSubscriptionRetryUsage(subscriptionId: number, groupId?: string): Promise { + await post(`/subscriptions/${subscriptionId}/resetretryusage`, { groupId: groupId ?? null }); }, async saveRetryAlertOverride( policyId: number, - input: { integrationId: number; groupId: string } & RetryAlertConfig, + input: { subscriptionId: number; groupId: string } & RetryAlertConfig, ): Promise { await post(`/retrypolicies/${policyId}/savealertoverride`, { - subscriptionId: input.integrationId, + subscriptionId: input.subscriptionId, groupId: input.groupId, alertMode: input.alertMode, alertHandlerId: input.alertHandlerId, diff --git a/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts b/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts index a8feb501..fd121823 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/subscriptionBody.ts @@ -1,12 +1,12 @@ -import type { InlineIntegrationDraft, Schedule } from "../types"; +import type { InlineSubscriptionDraft, Schedule } from "../types"; import { toRawMatchExpression } from "./matchExpression"; /** - * The subscription wire shape, in the one place both the integration endpoints and + * The subscription wire shape, in the one place both the subscription endpoints and * the gateway endpoints can reach. * - * It lives here rather than in `integrations.ts` because `gateways.ts` needs it too, - * and `integrations.ts` already imports `gateways.ts` — putting it there would make + * It lives here rather than in `subscriptions.ts` because `gateways.ts` needs it too, + * and `subscriptions.ts` already imports `gateways.ts` — putting it there would make * that cycle mutual. */ @@ -36,11 +36,11 @@ export const toRawSchedules = (schedules: Schedule[]): RawSchedule[] => })); /** - * An integration defined on a gateway's canvas, in the shape the gateway endpoints + * A subscription defined on a gateway's canvas, in the shape the gateway endpoints * take. No `documentId`: a bus gateway imposes its own, and the API-gateway caller * adds the one its picker chose. */ -export const inlineIntegrationBody = (d: InlineIntegrationDraft) => ({ +export const inlineSubscriptionBody = (d: InlineSubscriptionDraft) => ({ name: d.name.trim(), inactive: !d.enabled, workGroupId: d.workGroupId, @@ -56,6 +56,6 @@ export const inlineIntegrationBody = (d: InlineIntegrationDraft) => ({ handlerProperties: toKvArray(d.handlerProperties), matchExpression: toRawMatchExpression(d.matchExpression), schedules: toRawSchedules(d.schedules), - responseSubscriptionId: d.responseIntegrationId, + responseSubscriptionId: d.responseSubscriptionId, responseMessageTypeName: d.responseMessageTypeName, }); diff --git a/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts b/SW.Bitween.Web/ClientApp/src/api/http/subscriptions.ts similarity index 84% rename from SW.Bitween.Web/ClientApp/src/api/http/integrations.ts rename to SW.Bitween.Web/ClientApp/src/api/http/subscriptions.ts index 910ce616..0266ac9e 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/integrations.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/subscriptions.ts @@ -1,13 +1,13 @@ import type { ApiClient } from "../client"; import { ApiRequestError, - type Integration, - type IntegrationDetail, - type IntegrationInfo, - type IntegrationLastRun, - type IntegrationRow, - type IntegrationRun, - type IntegrationType, + type Subscription, + type SubscriptionDetail, + type SubscriptionInfo, + type SubscriptionLastRun, + type SubscriptionRow, + type SubscriptionRun, + type SubscriptionType, type InformationTypeRow, type Paged, type PartnerRow, @@ -96,7 +96,7 @@ interface RawSubscription { aggregationTarget?: string; } -const SUB_TYPE_BY_NUM: Record = { +const SUB_TYPE_BY_NUM: Record = { 1: "Internal", 2: "ApiCall", 4: "Receiving", @@ -104,7 +104,7 @@ const SUB_TYPE_BY_NUM: Record = { 16: "GatewayApiCall", 32: "BusGateway", }; -const INTEGRATION_TYPES: IntegrationType[] = [ +const SUBSCRIPTION_TYPES: SubscriptionType[] = [ "Receiving", "GatewayApiCall", "BusGateway", @@ -113,10 +113,10 @@ const INTEGRATION_TYPES: IntegrationType[] = [ "Aggregation", ]; /** Enums serialize as their C# member name string, but guard the numeric case too. */ -const toIntegrationType = (t: number | string): IntegrationType => +const toSubscriptionType = (t: number | string): SubscriptionType => typeof t === "number" ? (SUB_TYPE_BY_NUM[t] ?? "Internal") - : (INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"); + : (SUBSCRIPTION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"); // Empty-valued properties are dropped: an adapter property with no value means // "not set", and keeping it would make a freshly-cleared field compare unequal to @@ -133,11 +133,11 @@ const toSchedules = (raw: RawSchedule[] | null): Schedule[] => backwards: s.backwards, })); -function toIntegration(raw: RawSubscription, idOverride?: number): Integration { +function toSubscription(raw: RawSubscription, idOverride?: number): Subscription { return { id: raw.id ?? idOverride!, name: raw.name, - type: toIntegrationType(raw.type), + type: toSubscriptionType(raw.type), informationTypeId: raw.documentId, partnerId: raw.partnerId, enabled: !raw.inactive, @@ -154,7 +154,7 @@ function toIntegration(raw: RawSubscription, idOverride?: number): Integration { handlerProperties: toRecord(raw.handlerProperties), matchExpression: toMatchGroup(raw.matchExpression), schedules: toSchedules(raw.schedules), - responseIntegrationId: raw.responseSubscriptionId ?? null, + responseSubscriptionId: raw.responseSubscriptionId ?? null, responseMessageTypeName: raw.responseMessageTypeName ?? null, aggregationForId: raw.aggregationForId ?? null, isRunning: raw.isRunning ?? false, @@ -168,7 +168,7 @@ function toIntegration(raw: RawSubscription, idOverride?: number): Integration { async function fetchRaw(id: number): Promise { const raw = await get(`/subscriptions/${id}`); - if (!raw) throw new ApiRequestError("NOT_FOUND", "This integration no longer exists."); + if (!raw) throw new ApiRequestError("NOT_FOUND", "This subscription no longer exists."); return raw; } @@ -179,7 +179,7 @@ async function fetchAllRaw(): Promise { type UpdatableFields = Partial< Pick< - Integration, + Subscription, | "name" | "enabled" | "workGroupId" @@ -194,7 +194,7 @@ type UpdatableFields = Partial< | "handlerProperties" | "matchExpression" | "schedules" - | "responseIntegrationId" + | "responseSubscriptionId" | "responseMessageTypeName" > >; @@ -236,7 +236,7 @@ async function applyChanges(id: number, current: RawSubscription, changes: Updat changes.matchExpression !== undefined ? toRawMatchExpression(changes.matchExpression) : current.matchExpression, schedules: changes.schedules !== undefined ? toRawSchedules(changes.schedules) : (current.schedules ?? []), responseSubscriptionId: - changes.responseIntegrationId !== undefined ? changes.responseIntegrationId : current.responseSubscriptionId, + changes.responseSubscriptionId !== undefined ? changes.responseSubscriptionId : current.responseSubscriptionId, responseMessageTypeName: changes.responseMessageTypeName !== undefined ? changes.responseMessageTypeName : current.responseMessageTypeName, temporary: current.temporary, @@ -249,12 +249,12 @@ async function applyChanges(id: number, current: RawSubscription, changes: Updat }); } -function toIntegrationRow( +function toSubscriptionRow( raw: RawSubscription, infoTypeById: Map, partnerById: Map, -): IntegrationRow { - const type = toIntegrationType(raw.type); +): SubscriptionRow { + const type = toSubscriptionType(raw.type); const infoType = infoTypeById.get(raw.documentId); const partner = raw.partnerId !== null ? partnerById.get(raw.partnerId) : undefined; const schedules = toSchedules(raw.schedules); @@ -284,20 +284,20 @@ function toIntegrationRow( }; } -export const integrationMethods = { - async listIntegrations(): Promise { +export const subscriptionMethods = { + async listSubscriptions(): Promise { const rows = await fetchAllRaw(); return rows.map((raw) => ({ id: raw.id!, name: raw.name, - type: toIntegrationType(raw.type), + type: toSubscriptionType(raw.type), partnerIds: raw.partnerId !== null ? [raw.partnerId] : [], informationTypeId: raw.documentId, workGroupId: raw.workGroupId ?? null, retryPolicyId: raw.retryPolicyId ?? null, handlerId: raw.handlerId ?? null, responseMessageTypeName: raw.responseMessageTypeName ?? null, - responseIntegrationId: raw.responseSubscriptionId ?? null, + responseSubscriptionId: raw.responseSubscriptionId ?? null, // No backend endpoint indexes reference tokens, but the search rows carry // every adapter property, so the scan costs nothing extra here. ...scanReferenceTokens( @@ -311,7 +311,7 @@ export const integrationMethods = { })); }, - async listIntegrationRows(): Promise { + async listSubscriptionRows(): Promise { const [rows, infoTypes, partners] = await Promise.all([ fetchAllRaw(), documentMethods.listInformationTypes(), @@ -319,18 +319,18 @@ export const integrationMethods = { ]); const infoTypeById = new Map(infoTypes.map((t) => [t.id, t])); const partnerById = new Map(partners.map((p) => [p.id, p])); - return rows.map((raw) => toIntegrationRow(raw, infoTypeById, partnerById)); + return rows.map((raw) => toSubscriptionRow(raw, infoTypeById, partnerById)); }, - async searchIntegrationRows(query: { + async searchSubscriptionRows(query: { search: string; - type: IntegrationType | null; + type: SubscriptionType | null; informationTypeId?: number | null; partnerId?: number | null; inactive?: boolean | null; offset: number; limit: number; - }): Promise> { + }): Promise> { const qs = buildListQuery({ filters: [ ["Name", SEARCHY_RULE.contains, query.search.trim()], @@ -352,25 +352,25 @@ export const integrationMethods = { const partnerById = new Map(partners.map((p) => [p.id, p])); return { total: res.totalCount, - result: (res.result ?? []).map((raw) => toIntegrationRow(raw, infoTypeById, partnerById)), + result: (res.result ?? []).map((raw) => toSubscriptionRow(raw, infoTypeById, partnerById)), }; }, - async getIntegration(id: number): Promise { + async getSubscription(id: number): Promise { const [raw, apiGateways, busGateways, recentExchanges] = await Promise.all([ fetchRaw(id), gatewayMethods.listApiGateways(), gatewayMethods.listBusGateways(), - exchangeMethods.searchExchanges({ integrationId: id, offset: 0, limit: 8 }), + exchangeMethods.searchExchanges({ subscriptionId: id, offset: 0, limit: 8 }), ]); const infoType = await documentMethods.getInformationType(raw.documentId).catch(() => null); return { - ...toIntegration(raw, id), + ...toSubscription(raw, id), informationTypeCode: infoType?.code ?? infoType?.name ?? "", informationTypeName: infoType?.name ?? "", apiGatewayAttachments: apiGateways.flatMap((g) => g.attachments - .filter((a) => a.integrationId === id) + .filter((a) => a.subscriptionId === id) .map((a) => ({ gatewayId: g.id, gatewayName: g.name, @@ -381,7 +381,7 @@ export const integrationMethods = { ), busGatewayRoutes: busGateways.flatMap((g) => g.routes - .filter((r) => r.integrationId === id) + .filter((r) => r.subscriptionId === id) .map((r) => ({ gatewayId: g.id, gatewayName: g.name, partnerId: r.partnerId, partnerName: r.partnerName })), ), recentExchanges: recentExchanges.result.map((x) => ({ @@ -399,8 +399,8 @@ export const integrationMethods = { }; }, - async createIntegration(input: { - type: IntegrationType; + async createSubscription(input: { + type: SubscriptionType; name: string; informationTypeId: number; /** Required by the types that carry their own partner — Internal and ApiCall. */ @@ -415,10 +415,10 @@ export const integrationMethods = { handlerProperties?: Record; schedules?: Schedule[]; retryPolicyId?: number | null; - responseIntegrationId?: number | null; + responseSubscriptionId?: number | null; responseMessageTypeName?: string | null; enabled?: boolean; - }): Promise { + }): Promise { // One call, one transaction. This used to be a POST followed by a PATCH, // because create accepted only the name/type/document — and since the POST // committed on its own, a rejected PATCH left an empty subscription behind. @@ -443,35 +443,35 @@ export const integrationMethods = { schedules: input.schedules?.length ? toRawSchedules(input.schedules) : undefined, retryPolicyId: input.retryPolicyId ?? null, customRetryPolicy: null, - responseSubscriptionId: input.responseIntegrationId ?? null, + responseSubscriptionId: input.responseSubscriptionId ?? null, responseMessageTypeName: input.responseMessageTypeName ?? null, inactive: !(input.enabled ?? false), }); - return toIntegration(await fetchRaw(id), id); + return toSubscription(await fetchRaw(id), id); }, - async updateIntegration(id: number, changes: UpdatableFields): Promise { + async updateSubscription(id: number, changes: UpdatableFields): Promise { const current = await fetchRaw(id); await applyChanges(id, current, changes); - return toIntegration(await fetchRaw(id), id); + return toSubscription(await fetchRaw(id), id); }, - async deleteIntegration(id: number): Promise { + async deleteSubscription(id: number): Promise { await request(`/subscriptions/${id}`, { method: "DELETE" }); }, - async pauseIntegration(id: number): Promise { + async pauseSubscription(id: number): Promise { await post(`/subscriptions/${id}/pause`, {}); - return toIntegration(await fetchRaw(id), id); + return toSubscription(await fetchRaw(id), id); }, - async receiveNow(id: number): Promise { + async receiveNow(id: number): Promise { await post(`/subscriptions/${id}/receivenow`, {}); - return toIntegration(await fetchRaw(id), id); + return toSubscription(await fetchRaw(id), id); }, - listIntegrationRuns(id: number, limit = 20): Promise { - return get(`/subscriptions/runs?subscriptionId=${id}&limit=${limit}`); + listSubscriptionRuns(id: number, limit = 20): Promise { + return get(`/subscriptions/runs?subscriptionId=${id}&limit=${limit}`); }, async searchReceiveAttempts( @@ -502,19 +502,13 @@ export const integrationMethods = { }; }, - async listLastRuns(): Promise { - const rows = - await get<(Omit & { subscriptionId: number })[]>( - "/subscriptions/lastruns", - ); - return rows.map(({ subscriptionId, ...run }) => ({ ...run, integrationId: subscriptionId })); + // Both of these used to rename the wire's `subscriptionId` onto an `integrationId` field; + // now that the field carries the wire's own name, the rows come back already shaped. + async listLastRuns(): Promise { + return get("/subscriptions/lastruns"); }, async listScheduleHealth(): Promise { - const rows = - await get<(Omit & { subscriptionId: number })[]>( - "/subscriptions/schedulehealth", - ); - return rows.map(({ subscriptionId, ...health }) => ({ ...health, integrationId: subscriptionId })); + return get("/subscriptions/schedulehealth"); }, } satisfies Partial; diff --git a/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts b/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts index 51b9a303..0e39494d 100644 --- a/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts +++ b/SW.Bitween.Web/ClientApp/src/api/http/workGroups.ts @@ -1,7 +1,7 @@ import type { ApiClient } from "../client"; import { ApiRequestError, - type IntegrationType, + type SubscriptionType, type WorkGroup, type WorkGroupDetail, type WorkGroupRow, @@ -26,7 +26,7 @@ interface RawSubscriptionRef { type: number | string; } -const SUB_TYPE_BY_NUM: Record = { +const SUB_TYPE_BY_NUM: Record = { 1: "Internal", 2: "ApiCall", 4: "Receiving", @@ -34,7 +34,7 @@ const SUB_TYPE_BY_NUM: Record = { 16: "GatewayApiCall", 32: "BusGateway", }; -const INTEGRATION_TYPES: IntegrationType[] = [ +const SUBSCRIPTION_TYPES: SubscriptionType[] = [ "Receiving", "GatewayApiCall", "BusGateway", @@ -43,9 +43,9 @@ const INTEGRATION_TYPES: IntegrationType[] = [ "Aggregation", ]; /** Enums may arrive as the numeric value or the name in any case. */ -const toIntegrationType = (t: number | string): IntegrationType => { +const toSubscriptionType = (t: number | string): SubscriptionType => { if (typeof t === "number") return SUB_TYPE_BY_NUM[t] ?? "Internal"; - return INTEGRATION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; + return SUBSCRIPTION_TYPES.find((k) => k.toLowerCase() === t.toLowerCase()) ?? "Internal"; }; async function fetchSubscriptionsByWorkGroup(workGroupId: number): Promise { @@ -141,7 +141,7 @@ export const workGroupMethods = { if (!w) throw new ApiRequestError("NOT_FOUND", "This work group no longer exists."); return { ...toWorkGroup(w), - integrations: subs.map((s) => ({ id: s.id, name: s.name, type: toIntegrationType(s.type) })), + subscriptions: subs.map((s) => ({ id: s.id, name: s.name, type: toSubscriptionType(s.type) })), }; }, diff --git a/SW.Bitween.Web/ClientApp/src/api/permissions.ts b/SW.Bitween.Web/ClientApp/src/api/permissions.ts index a1cb5923..dec228ae 100644 --- a/SW.Bitween.Web/ClientApp/src/api/permissions.ts +++ b/SW.Bitween.Web/ClientApp/src/api/permissions.ts @@ -40,7 +40,7 @@ export const groupsIn = (areas: PermissionArea[]): string[] => [ ...new Set(areas.map((area) => area.group)), ]; -/** "Integrations · Edit" for a key, falling back to the raw key for anything unrecognised. */ +/** "Subscriptions · Edit" for a key, falling back to the raw key for anything unrecognised. */ export const labelIn = (areas: PermissionArea[], key: PermissionKey): string => { const [areaId, actionId] = key.split("."); const area = areas.find((a) => a.id === areaId); diff --git a/SW.Bitween.Web/ClientApp/src/api/types.ts b/SW.Bitween.Web/ClientApp/src/api/types.ts index 4e35d18c..d748d454 100644 --- a/SW.Bitween.Web/ClientApp/src/api/types.ts +++ b/SW.Bitween.Web/ClientApp/src/api/types.ts @@ -91,10 +91,10 @@ export class NotWiredError extends Error { // ——— Configuration entities (sub-phase 2) ——— /** Lightweight references for "used by" panels. */ -export interface IntegrationSetupRef { +export interface SubscriptionSetupRef { id: number; name: string; - type: IntegrationType; + type: SubscriptionType; } export interface ApiGatewayAttachmentRef { gatewayId: number; @@ -125,18 +125,18 @@ export interface ExchangeRef { } /** - * Lightweight summary of every integration, cached client-side so pages + * Lightweight summary of every subscription, cached client-side so pages * can answer "who uses this property/value/policy?" without new requests. * Derived server-side by scanning adapter property values for tokens. */ -export interface IntegrationInfo { +export interface SubscriptionInfo { id: number; name: string; - type: IntegrationType; + type: SubscriptionType; /** - * The integration's OWN partner, which only the legacy types carry. Partners + * The subscription's OWN partner, which only the legacy types carry. Partners * linked through a gateway attachment or a bus route are NOT here — the list - * endpoint doesn't know about them. Use `usePartnerIntegrations()` when you + * endpoint doesn't know about them. Use `usePartnerSubscriptions()` when you * need the full picture. */ partnerIds: number[]; @@ -151,8 +151,8 @@ export interface IntegrationInfo { handlerId: string | null; /** The bus message its delivery response is published as, if any. */ responseMessageTypeName: string | null; - /** The integration its delivery response is handed straight to, if any. */ - responseIntegrationId: number | null; + /** The subscription its delivery response is handed straight to, if any. */ + responseSubscriptionId: number | null; /** * Reference tokens found in its adapter properties. Both are matched * case-insensitively, as the backend resolver does — compare them with @@ -219,7 +219,7 @@ export interface InformationTypeRow extends InformationType { usedByCount: number; } export interface InformationTypeDetail extends InformationType { - integrationSetups: IntegrationSetupRef[]; + subscriptionSetups: SubscriptionSetupRef[]; busGateways: { gatewayId: number; gatewayName: string }[]; trail: TrailEntry[]; recentExchanges: ExchangeRef[]; @@ -235,7 +235,7 @@ export interface GlobalValuesSet { /** Alias the ported mapper code types its global-set props with. */ export type GlobalValuesSetRow = GlobalValuesSet; export interface ValueSetUsage { - integrationSetup: IntegrationSetupRef; + subscriptionSetup: SubscriptionSetupRef; keys: string[]; } export interface GlobalValuesSetDetail extends GlobalValuesSet { @@ -260,7 +260,7 @@ export type RetryDelay = /** * Whether a level of the alert hierarchy names its own destination or defers upward. * - * Resolved most-specific-first per integration and group: the pair's own override, then + * Resolved most-specific-first per subscription and group: the pair's own override, then * the group, then the policy. A level that sends **replaces** the one above rather than * merging with it, so whichever level wins has to carry the handler and every property * it needs. @@ -289,7 +289,7 @@ export interface RetryGroup { action: "Allow" | "Block"; budget?: { maxAttemptsPerError: number; maxAttemptsTotal: number; delay: RetryDelay }; notes?: string; - /** Where this group's budget-exhausted alert goes, for every integration using the policy. */ + /** Where this group's budget-exhausted alert goes, for every subscription using the policy. */ alertMode: RetryAlertMode; alertHandlerId: string | null; alertHandlerProperties: Record; @@ -313,7 +313,7 @@ export interface RetryPolicyListRow { usedByCount: number; } export interface RetryPolicyDetail extends RetryPolicy { - integrations: IntegrationSetupRef[]; + subscriptions: SubscriptionSetupRef[]; } /** @@ -332,22 +332,22 @@ export interface RetryAlertOutcome { } /** - * The whole state of one integration-and-group pair: how much of the group's budget that - * integration has spent, and where the pair's budget-exhausted alert would go. + * The whole state of one subscription-and-group pair: how much of the group's budget that + * subscription has spent, and where the pair's budget-exhausted alert would go. * - * Budgets are counted per pair — a shared policy gives every integration its own separate total + * Budgets are counted per pair — a shared policy gives every subscription its own separate total * — so there is no such thing as "this policy's usage". Any single figure on a policy or a group * would be an aggregate matching nothing anyone can act on, which is why the pair is also what * resetting and overriding both address. */ export interface RetryUsageRow { - integrationId: number; - integrationName: string; + subscriptionId: number; + subscriptionName: string; groupId: string; groupName: string; used: number; total: number; - /** Spent out: this integration gets no further automatic retries from this group. */ + /** Spent out: this subscription gets no further automatic retries from this group. */ exhausted: boolean; /** Null when the pair has never failed — also how you know there is no counter to reset. */ lastAttemptOn: string | null; @@ -377,7 +377,7 @@ export interface RetryAttempt { export interface RetryAttempts { /** - * Every failure this group has caught for this integration. Failures outlive the counter, + * Every failure this group has caught for this subscription. Failures outlive the counter, * which is reset, so this is not the counter's value. */ total: number; @@ -405,8 +405,8 @@ export interface Notifier { onSuccess: boolean; channelId: string; channelProperties: Record; - /** Integrations this notifier watches; empty = it never fires. */ - integrationIds: number[]; + /** Subscriptions this notifier watches; empty = it never fires. */ + subscriptionIds: number[]; createdOn: string; } @@ -422,18 +422,18 @@ export interface NotifierDetail extends Notifier { recentNotifications: NotificationEntry[]; } -// ——— Integrations (subscriptions) ——— +// ——— Subscriptions (subscriptions) ——— /** * Backend Subscription.Type. Aggregation exists in data but is deferred in * this UI; Internal and ApiCall are legacy — shown and editable, never created. */ /** - * The editable fields of an integration being defined inline, while whatever points + * The editable fields of a subscription being defined inline, while whatever points * at it is being made. Mirrors the studio's own draft — deliberately, so the canvas * can hand its draft straight to the client. */ -export interface InlineIntegrationDraft { +export interface InlineSubscriptionDraft { name: string; enabled: boolean; workGroupId: number | null; @@ -448,11 +448,11 @@ export interface InlineIntegrationDraft { handlerProperties: Record; matchExpression: MatchGroup | null; schedules: Schedule[]; - responseIntegrationId: number | null; + responseSubscriptionId: number | null; responseMessageTypeName: string | null; } -export type IntegrationType = +export type SubscriptionType = | "Receiving" | "GatewayApiCall" | "BusGateway" @@ -511,10 +511,10 @@ export interface Schedule { backwards: boolean; } -export interface Integration { +export interface Subscription { id: number; name: string; - type: IntegrationType; + type: SubscriptionType; informationTypeId: number; /** Direct partner — legacy Internal/ApiCall (and Aggregation) only. */ partnerId: number | null; @@ -532,12 +532,12 @@ export interface Integration { mapperProperties: Record; handlerId: string | null; handlerProperties: Record; - /** Legacy Internal only: which documents this integration picks up. */ + /** Legacy Internal only: which documents this subscription picks up. */ matchExpression: MatchGroup | null; /** Receiving (and Aggregation) only. */ schedules: Schedule[]; - /** Feed the handler's response into another integration. */ - responseIntegrationId: number | null; + /** Feed the handler's response into another subscription. */ + responseSubscriptionId: number | null; responseMessageTypeName: string | null; aggregationForId: number | null; // — health (read-only) — @@ -549,10 +549,10 @@ export interface Integration { createdOn: string; } -export interface IntegrationRow { +export interface SubscriptionRow { id: number; name: string; - type: IntegrationType; + type: SubscriptionType; informationTypeId: number; informationTypeCode: string; partners: { id: number; name: string }[]; @@ -568,10 +568,10 @@ export interface IntegrationRow { } /** - * One execution of a scheduled integration, from the scheduler's own history. + * One execution of a scheduled subscription, from the scheduler's own history. * Kept for `RetentionDays` (~30) — older runs are purged, not archived here. */ -export interface IntegrationRun { +export interface SubscriptionRun { startedOn: string; endedOn: string | null; durationMs: number | null; @@ -583,14 +583,14 @@ export interface IntegrationRun { manual: boolean; } -export interface IntegrationLastRun extends IntegrationRun { - integrationId: number; +export interface SubscriptionLastRun extends SubscriptionRun { + subscriptionId: number; /** Finished runs in the recent window; in-progress runs count as neither pass nor fail. */ recentTotal: number; recentSucceeded: number; } -/** One poll of a Receiving integration's own receive step — independent of the scheduler's +/** One poll of a Receiving subscription's own receive step — independent of the scheduler's * run history, which only knows whether the method threw (it never does; failures here are * caught and reported this way instead). */ export type ReceiveOutcome = "Failed" | "NoNewData" | "Received"; @@ -611,12 +611,12 @@ export interface ReceiveAttemptRow { } /** - * Whether a scheduled integration will actually fire, straight from the scheduler. - * Everything here can disagree with what the integration's own record says, and + * Whether a scheduled subscription will actually fire, straight from the scheduler. + * Everything here can disagree with what the subscription's own record says, and * when it does the job is silently dead rather than visibly broken. */ export interface ScheduleHealth { - integrationId: number; + subscriptionId: number; scheduleCount: number; /** Fewer than `scheduleCount` means a schedule exists that nothing will ever fire. */ triggerCount: number; @@ -627,10 +627,10 @@ export interface ScheduleHealth { stuck: boolean; } -export interface IntegrationDetail extends Integration { +export interface SubscriptionDetail extends Subscription { informationTypeCode: string; informationTypeName: string; - /** Where this integration is plugged in (entry points). */ + /** Where this subscription is plugged in (entry points). */ apiGatewayAttachments: { gatewayId: number; gatewayName: string; urlName: string; partnerId: number; partnerName: string }[]; busGatewayRoutes: { gatewayId: number; gatewayName: string; partnerId: number | null; partnerName: string | null }[]; watchingNotifiers: { id: number; name: string }[]; @@ -661,7 +661,7 @@ export interface WorkGroupRow extends WorkGroup { consumerCount: number; } export interface WorkGroupDetail extends WorkGroup { - integrations: IntegrationSetupRef[]; + subscriptions: SubscriptionSetupRef[]; } // ——— API gateways ——— @@ -669,8 +669,8 @@ export interface WorkGroupDetail extends WorkGroup { export interface ApiGatewayAttachment { partnerId: number; partnerName: string; - integrationId: number; - integrationName: string; + subscriptionId: number; + subscriptionName: string; } export interface ApiGateway { id: number; @@ -692,8 +692,8 @@ export interface ApiGatewayDetail extends ApiGateway { export interface BusGatewayRoute { id: number; - integrationId: number; - integrationName: string; + subscriptionId: number; + subscriptionName: string; partnerId: number | null; partnerName: string | null; /** null = route matches every message of the gateway's type. */ @@ -782,8 +782,8 @@ export interface ExchangeFileRef { export interface ExchangeRow { id: string; status: ExchangeStatus; - integrationId: number | null; - integrationName: string | null; + subscriptionId: number | null; + subscriptionName: string | null; informationTypeId: number; informationTypeCode: string; partnerId: number | null; @@ -800,7 +800,7 @@ export interface ExchangeRow { scheduledRetryOn: string | null; exception: string | null; promotedProperties: Record | null; - /** True when the integration has no mapper — the Mapped stage is skipped. */ + /** True when the subscription has no mapper — the Mapped stage is skipped. */ mapperSkipped: boolean; files: { input: ExchangeFileRef | null; @@ -811,7 +811,7 @@ export interface ExchangeRow { export interface ExchangeQuery { status?: ExchangeStatus; - integrationId?: number; + subscriptionId?: number; partnerId?: number; informationTypeId?: number; /** Comma/pipe/newline separated; matches id, retryFor OR aggregationXchangeId. */ @@ -843,8 +843,8 @@ export interface ScheduledRetryRow { id: string; /** When the retry job will pick it up. */ on: string; - integrationId: number | null; - integrationName: string | null; + subscriptionId: number | null; + subscriptionName: string | null; informationTypeId: number; informationTypeCode: string; exception: string | null; @@ -853,8 +853,8 @@ export interface ScheduledRetryRow { /** What the exchange carries — how a pending retry identifies itself in a list. */ promotedProperties: Record | null; /** - * The shared retry policy the integration currently points at. Null when the - * policy is defined inline on the integration instead, so the integration — not + * The shared retry policy the subscription currently points at. Null when the + * policy is defined inline on the subscription instead, so the subscription — not * the Retry policies list — is where to go and look. */ retryPolicyId: number | null; @@ -862,7 +862,7 @@ export interface ScheduledRetryRow { } export interface ScheduledRetryQuery { - integrationId?: number; + subscriptionId?: number; informationTypeId?: number; /** Substring match against the exception text. */ exception?: string; @@ -984,19 +984,19 @@ export interface DashboardData { queueAlerts: number; /** Last 14 days, oldest first; today is the final entry. */ trafficByDay: { date: string; success: number; failed: number }[]; - /** Top integrations by 7-day traffic, busiest first. */ + /** Top subscriptions by 7-day traffic, busiest first. */ busiest: { id: number; name: string; count: number; failed: number }[]; latestFailures: { id: string; status: ExchangeStatus; - integrationId: number | null; - integrationName: string | null; + subscriptionId: number | null; + subscriptionName: string | null; informationTypeCode: string; on: string; exception: string | null; }[]; attention: { - failingIntegrations: { id: number; name: string; consecutiveFailures: number }[]; - pausedIntegrations: { id: number; name: string }[]; + failingSubscriptions: { id: number; name: string; consecutiveFailures: number }[]; + pausedSubscriptions: { id: number; name: string }[]; }; } diff --git a/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx b/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx index 0156c04a..bb35f0cd 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/AdapterConfig.tsx @@ -420,7 +420,7 @@ export function AdapterConfig({ /** What "no adapter" means here, e.g. "None — payload passes through unchanged". */ noneLabel?: string; /** When the native JSON mapper is selected, where its visual editor lives. */ - /** Null while the integration is still a draft — there is no page to open yet. */ + /** Null while the subscription is still a draft — there is no page to open yet. */ mapperEditorHref?: string | null; }) { const catalog = useAdapterCatalog(kind); diff --git a/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeFields.tsx b/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeFields.tsx index 3b042f96..7d0fc26f 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeFields.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/InformationTypeFields.tsx @@ -109,7 +109,7 @@ export function InformationTypeFields({ (null); @@ -78,7 +78,7 @@ export function PartnerFields({ }, }); - const users = partnerId === null ? [] : (partnerIntegrations.get(partnerId) ?? []); + const users = partnerId === null ? [] : (partnerSubscriptions.get(partnerId) ?? []); return (
@@ -113,9 +113,9 @@ export function PartnerFields({ rowDetails={(row) => { if (!row.key.trim() || partnerId === null) return null; return ( - referencesPartnerProp(s, row.key.trim()))} - emptyText="Not referenced by any integration — safe to change or remove." + emptyText="Not referenced by any subscription — safe to change or remove." /> ); }} diff --git a/SW.Bitween.Web/ClientApp/src/components/config/IntegrationDialog.tsx b/SW.Bitween.Web/ClientApp/src/components/config/SubscriptionDialog.tsx similarity index 84% rename from SW.Bitween.Web/ClientApp/src/components/config/IntegrationDialog.tsx rename to SW.Bitween.Web/ClientApp/src/components/config/SubscriptionDialog.tsx index b10a4879..c1ddfd14 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/IntegrationDialog.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/SubscriptionDialog.tsx @@ -1,32 +1,32 @@ import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; -import { api, type IntegrationType } from "../../api"; +import { api, type SubscriptionType } from "../../api"; import { Button, FormError } from "../ui/basics"; import { Field, TextInput } from "../ui/forms"; import { Dialog } from "../ui/overlays"; import { CodeBadge } from "../ui/Panel"; import { AdapterConfig, useAdapterCatalog } from "./AdapterConfig"; import { InfoTypePicker } from "./pickers"; -import { adapterIncomplete } from "../../pages/integrations/studio/faces"; +import { adapterIncomplete } from "../../pages/subscriptions/studio/faces"; /** - * A new gateway-backed integration, asked down to what it cannot run without: a + * A new gateway-backed subscription, asked down to what it cannot run without: a * name, the information type it carries, and somewhere to deliver. * * Deliberately not the whole pipeline. Transformation, validation and response are - * nodes on the integration's own studio the moment it exists, and reproducing the + * nodes on the subscription's own studio the moment it exists, and reproducing the * rail inside a modal would be a worse copy of a surface that already works. The * dialog closes on create and hands the id back, so the picker that opened it - * selects the new integration and you carry on. + * selects the new subscription and you carry on. */ -export function IntegrationDialog({ +export function SubscriptionDialog({ type, informationTypeId, onClose, onCreated, }: { - type: Extract; + type: Extract; /** Fixed by the caller (a bus gateway's own type); otherwise it is asked for. */ informationTypeId?: number; onClose: () => void; @@ -49,7 +49,7 @@ export function IntegrationDialog({ const create = useMutation({ mutationFn: () => - api.createIntegration({ + api.createSubscription({ type, name: name.trim(), informationTypeId: pickedTypeId!, @@ -61,9 +61,9 @@ export function IntegrationDialog({ enabled: true, }), onSuccess: (created) => { - void queryClient.invalidateQueries({ queryKey: ["integrations"] }); - void queryClient.invalidateQueries({ queryKey: ["integration-rows"] }); - void queryClient.invalidateQueries({ queryKey: ["integration-rows-search"] }); + void queryClient.invalidateQueries({ queryKey: ["subscriptions"] }); + void queryClient.invalidateQueries({ queryKey: ["subscription-rows"] }); + void queryClient.invalidateQueries({ queryKey: ["subscription-rows-search"] }); onCreated(created.id); onClose(); }, @@ -77,7 +77,7 @@ export function IntegrationDialog({ ].filter((m): m is string => typeof m === "string"); return ( - +

{type === "GatewayApiCall" @@ -103,7 +103,7 @@ export function IntegrationDialog({

) : ( - + )} @@ -139,7 +139,7 @@ export function IntegrationDialog({ disabled={missing.length > 0} onClick={() => create.mutate()} > - Create integration + Create subscription
diff --git a/SW.Bitween.Web/ClientApp/src/components/config/WorkGroupDialog.tsx b/SW.Bitween.Web/ClientApp/src/components/config/WorkGroupDialog.tsx index 3d49b1ae..1c4a765f 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/WorkGroupDialog.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/WorkGroupDialog.tsx @@ -10,7 +10,7 @@ import { suggestSlug } from "../../lib/identifiers"; /** * A work group's editable settings, as one component. * - * Shared by the group's own page and the dialog opened from an integration, so + * Shared by the group's own page and the dialog opened from a subscription, so * there is one definition of what a work group is. Its live queue stats and * used-by list stay on the page. */ @@ -115,7 +115,7 @@ export function WorkGroupFields({ const EMPTY: WorkGroupDraft = { name: "", busMessageName: "", prefetch: 10, priority: 5 }; -/** A work group, created or edited in place — reached from an integration's lane picker. */ +/** A work group, created or edited in place — reached from a subscription's lane picker. */ export function WorkGroupDialog({ groupId, onClose, @@ -172,7 +172,7 @@ export function WorkGroupDialog({ ) : (

- Gives its own queue, priority and prefetch to whatever integrations you assign to it. + Gives its own queue, priority and prefetch to whatever subscriptions you assign to it.

{save.error?.message} diff --git a/SW.Bitween.Web/ClientApp/src/components/config/pickers.tsx b/SW.Bitween.Web/ClientApp/src/components/config/pickers.tsx index 9f23d49c..906e3839 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/pickers.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/pickers.tsx @@ -6,9 +6,9 @@ import { api, type InformationTypeRow } from "../../api"; import { useSessionCan } from "../../auth/guards"; import { SearchSelect } from "../ui/SearchSelect"; import { InformationTypeDialog } from "./InformationTypeDialog"; -import { IntegrationDialog } from "./IntegrationDialog"; +import { SubscriptionDialog } from "./SubscriptionDialog"; import { PartnerDialog } from "./PartnerDialog"; -import { useIntegrationsCache } from "./shared"; +import { useSubscriptionsCache } from "./shared"; /* * Pick-one controls used inside flows. Creating or amending the thing you are @@ -123,35 +123,35 @@ export function InfoTypePicker({ } /** - * Pick-one GatewayApiCall/BusGateway integration behind an entry point (an API + * Pick-one GatewayApiCall/BusGateway subscription behind an entry point (an API * gateway attachment or a bus gateway route). Creating one opens a dialog. */ -export function IntegrationPicker({ +export function SubscriptionPicker({ type, informationTypeId, value, onChange, id, /** - * Given, "New integration" defines one where the caller stands instead of opening a + * Given, "New subscription" defines one where the caller stands instead of opening a * dialog — the caller renders its fields and saves it with whatever points at it. */ onDefineHere, }: { type: "GatewayApiCall" | "BusGateway"; - /** Bus routes only run integrations carrying the gateway's own information type. */ + /** Bus routes only run subscriptions carrying the gateway's own information type. */ informationTypeId?: number; value: number | null; onChange: (id: number) => void; id?: string; onDefineHere?: () => void; }) { - const integrations = useIntegrationsCache(); + const subscriptions = useSubscriptionsCache(); const infoTypes = useQuery({ queryKey: ["information-types"], queryFn: () => api.listInformationTypes() }); const canCreate = useSessionCan("subscriptions.create"); const [creating, setCreating] = useState(false); - const candidates = (integrations.data ?? []).filter( + const candidates = (subscriptions.data ?? []).filter( (s) => s.type === type && (informationTypeId === undefined || s.informationTypeId === informationTypeId), ); @@ -159,27 +159,27 @@ export function IntegrationPicker({
v !== "" && onChange(Number(v))} - placeholder="Pick an integration…" + placeholder="Pick a subscription…" options={candidates.map((s) => ({ value: String(s.id), label: s.name, hint: `Carries ${infoTypes.data?.find((t) => t.id === s.informationTypeId)?.code ?? "…"}`, }))} /> - {/* An integration keeps a "View": its page holds run history and traffic that + {/* A subscription keeps a "View": its page holds run history and traffic that no dialog is going to show, and going there is the user's own choice. */} (onDefineHere ? onDefineHere() : setCreating(true)), }, @@ -188,7 +188,7 @@ export function IntegrationPicker({ } /> {creating && ( - setCreating(false)} diff --git a/SW.Bitween.Web/ClientApp/src/components/config/shared.tsx b/SW.Bitween.Web/ClientApp/src/components/config/shared.tsx index 4895b778..66c49e95 100644 --- a/SW.Bitween.Web/ClientApp/src/components/config/shared.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/config/shared.tsx @@ -4,10 +4,10 @@ import { useQuery } from "@tanstack/react-query"; import { api, type ExchangeRef, - type IntegrationInfo, - type IntegrationRow, - type IntegrationSetupRef, - type IntegrationType, + type SubscriptionInfo, + type SubscriptionRow, + type SubscriptionSetupRef, + type SubscriptionType, type QueueSeverity, type ScheduleHealth, type TrailEntry, @@ -19,13 +19,13 @@ import { MiniTable, type Column } from "../ui/Table"; import { formatDate, timeAgo } from "../../lib/dates"; /** - * Display names for integration types; Internal and ApiCall are legacy. + * Display names for subscription types; Internal and ApiCall are legacy. * * `Receiving` reads "Scheduled job", not the backend's "Receiver" — it is the * same thing the sidebar and its own page call a scheduled job, and one entity * with two names in the same screen is just a puzzle for the reader. */ -export const INTEGRATION_TYPE_LABELS: Record = { +export const SUBSCRIPTION_TYPE_LABELS: Record = { Receiving: "Scheduled job", GatewayApiCall: "API gateway", BusGateway: "Bus gateway", @@ -34,20 +34,20 @@ export const INTEGRATION_TYPE_LABELS: Record = { Aggregation: "Aggregation", }; -export const isLegacyType = (type: IntegrationType) => +export const isLegacyType = (type: SubscriptionType) => type === "Internal" || type === "ApiCall"; -export function TypeBadge({ type }: { type: IntegrationType }) { +export function TypeBadge({ type }: { type: SubscriptionType }) { return ( - {INTEGRATION_TYPE_LABELS[type]} + {SUBSCRIPTION_TYPE_LABELS[type]} {isLegacyType(type) && Legacy} ); } -/** Enabled/paused pair — an integration can be both enabled and paused. */ -export function IntegrationStatusBadges({ +/** Enabled/paused pair — a subscription can be both enabled and paused. */ +export function SubscriptionStatusBadges({ enabled, paused, }: { @@ -75,7 +75,7 @@ export function IntegrationStatusBadges({ /** * A fault the scheduler itself reports, which contradicts whatever the - * integration's own badges say — "Active" with no trigger behind it is still a + * subscription's own badges say — "Active" with no trigger behind it is still a * job that never runs. Shared by the scheduled-jobs table and the pipeline rail * so the two can't drift apart. */ @@ -112,7 +112,7 @@ export function scheduleFault( label: "Trigger paused", tone: "warn", title: - "Paused inside the scheduler — this is not the integration's own pause.", + "Paused inside the scheduler — this is not the subscription's own pause.", }; case "Blocked": return { @@ -250,7 +250,7 @@ export function PromotedProps({ } /** - * Recent exchanges for one partner / information type / integration. + * Recent exchanges for one partner / information type / subscription. * * Leads with the promoted properties, because they are the only column that says * what the exchange *was*. The id led before and answered the question nobody @@ -350,16 +350,16 @@ export function ExchangesList({ ); } -/** Integrations referencing this entity, each linking to its page. */ -export function SetupList({ items }: { items: IntegrationSetupRef[] }) { +/** Subscriptions referencing this entity, each linking to its page. */ +export function SetupList({ items }: { items: SubscriptionSetupRef[] }) { return ( s.id} - empty="Not used by any integration." + empty="Not used by any subscription." columns={[ { - header: "Integration", + header: "Subscription", truncate: true, cell: (s) => ( api.listIntegrations(), + queryKey: ["subscriptions"], + queryFn: () => api.listSubscriptions(), staleTime: Infinity, }); } /** - * Which integrations each partner is reached through, keyed by partner id. + * Which subscriptions each partner is reached through, keyed by partner id. * * A subscription's own `partnerId` only covers the legacy types. Everything * modern links a partner through a **gateway** — an API-gateway attachment or a * bus route — so those have to be folded in or a partner that is plainly in use * shows up as unused. Both gateway lists are the same cache entries the - * Integrations page fills, and each is gated on its own view permission. + * Subscriptions page fills, and each is gated on its own view permission. */ -export function usePartnerIntegrations(): Map { - const integrations = useIntegrationsCache().data ?? []; +export function usePartnerSubscriptions(): Map { + const subscriptions = useSubscriptionsCache().data ?? []; const canSeeApi = useSessionCan("api-gateways.view"); const canSeeBus = useSessionCan("bus-gateways.view"); const apiGateways = @@ -419,11 +419,11 @@ export function usePartnerIntegrations(): Map { }).data ?? []; return useMemo(() => { - const byId = new Map(integrations.map((s) => [s.id, s])); - const out = new Map(); - const add = (partnerId: number | null, integrationId: number) => { + const byId = new Map(subscriptions.map((s) => [s.id, s])); + const out = new Map(); + const add = (partnerId: number | null, subscriptionId: number) => { if (partnerId === null) return; - const setup = byId.get(integrationId); + const setup = byId.get(subscriptionId); if (!setup) return; const list = out.get(partnerId) ?? []; if (!list.some((x) => x.id === setup.id)) { @@ -431,22 +431,22 @@ export function usePartnerIntegrations(): Map { out.set(partnerId, list); } }; - for (const s of integrations) + for (const s of subscriptions) for (const pid of s.partnerIds) add(pid, s.id); for (const g of apiGateways) - for (const a of g.attachments) add(a.partnerId, a.integrationId); + for (const a of g.attachments) add(a.partnerId, a.subscriptionId); for (const g of busGateways) - for (const r of g.routes) add(r.partnerId, r.integrationId); + for (const r of g.routes) add(r.partnerId, r.subscriptionId); return out; - }, [integrations, apiGateways, busGateways]); + }, [subscriptions, apiGateways, busGateways]); } /** - * The same wiring as `usePartnerIntegrations`, read the other way: partners - * reached through a gateway, keyed by *integration* id. + * The same wiring as `usePartnerSubscriptions`, read the other way: partners + * reached through a gateway, keyed by *subscription* id. * - * `IntegrationRow.partners` only carries a subscription's own `partnerId`, which - * the modern types never have — without this, every gateway-fed integration + * `SubscriptionRow.partners` only carries a subscription's own `partnerId`, which + * the modern types never have — without this, every gateway-fed subscription * shows a dash where its partner should be. */ export function useGatewayPartners(): Map< @@ -471,33 +471,33 @@ export function useGatewayPartners(): Map< return useMemo(() => { const out = new Map(); const add = ( - integrationId: number, + subscriptionId: number, partnerId: number | null, partnerName: string | null, ) => { if (partnerId === null || partnerName === null) return; - const list = out.get(integrationId) ?? []; + const list = out.get(subscriptionId) ?? []; if (!list.some((p) => p.id === partnerId)) { list.push({ id: partnerId, name: partnerName }); - out.set(integrationId, list); + out.set(subscriptionId, list); } }; for (const g of apiGateways) for (const a of g.attachments) - add(a.integrationId, a.partnerId, a.partnerName); + add(a.subscriptionId, a.partnerId, a.partnerName); for (const g of busGateways) for (const r of g.routes) - add(r.integrationId, r.partnerId, r.partnerName); + add(r.subscriptionId, r.partnerId, r.partnerName); return out; }, [apiGateways, busGateways]); } -/** Live status for every integration, keyed by id — shared by the gateway pages. */ -export function useIntegrationRowsById(): Map { +/** Live status for every subscription, keyed by id — shared by the gateway pages. */ +export function useSubscriptionRowsById(): Map { const rows = useQuery({ - queryKey: ["integration-rows"], - queryFn: () => api.listIntegrationRows(), + queryKey: ["subscription-rows"], + queryFn: () => api.listSubscriptionRows(), }).data ?? []; return useMemo(() => new Map(rows.map((r) => [r.id, r])), [rows]); } @@ -538,28 +538,28 @@ export function WiredHealthBadge({ rows, empty, }: { - rows: IntegrationRow[]; + rows: SubscriptionRow[]; empty: string; }) { if (rows.length === 0) return {empty}; const failing = rows.filter((r) => r.consecutiveFailures > 0).length; if (failing > 0) return ( - + {failing} failing ); const paused = rows.filter((r) => r.paused).length; if (paused > 0) return ( - + {paused} paused ); const disabled = rows.filter((r) => !r.enabled).length; if (disabled > 0) return ( - {disabled} disabled + {disabled} disabled ); return ( @@ -572,21 +572,21 @@ export function WiredHealthBadge({ * The columns describing the pipeline behind one gateway attachment or route. * * These tables are the only 1:1 place in the gateway story — one row is exactly - * one partner and one integration — so this is where its configuration can be + * one partner and one subscription — so this is where its configuration can be * stated in separate columns without the reader having to guess which value * pairs with which. The parent list can't do it: two parallel lists in a row * lose their pairing, which is why the gateway tables carry only aggregates. * - * Everything here comes from caches the app already holds, keyed by integration + * Everything here comes from caches the app already holds, keyed by subscription * id; the gateway endpoints know none of it. */ -export function useWiredIntegrationColumns( - integrationIdOf: (row: T) => number, +export function useWiredSubscriptionColumns( + subscriptionIdOf: (row: T) => number, /** Off where the parent already fixes it — a bus gateway listens for one type. */ { informationType = true }: { informationType?: boolean } = {}, ): Column[] { - const rowsById = useIntegrationRowsById(); - const setups = useIntegrationsCache().data ?? []; + const rowsById = useSubscriptionRowsById(); + const setups = useSubscriptionsCache().data ?? []; const setupById = useMemo( () => new Map(setups.map((s) => [s.id, s])), [setups], @@ -601,7 +601,7 @@ export function useWiredIntegrationColumns( columns.push({ header: "Information type", cell: (row) => { - const r = rowsById.get(integrationIdOf(row)); + const r = rowsById.get(subscriptionIdOf(row)); if (!r) return ; return canSeeInfoTypes ? ( ( { header: "Work group", cell: (row) => { - const id = setupById.get(integrationIdOf(row))?.workGroupId ?? null; + const id = setupById.get(subscriptionIdOf(row))?.workGroupId ?? null; // "Ungrouped", not "Default": a null WorkGroupId isn't the absence of a // lane, it's `WorkGroup.None` — a real shared queue (`0Ungrouped`) that - // every ungrouped integration competes in. Matches the wording the - // integration page's work-group picker already uses. + // every ungrouped subscription competes in. Matches the wording the + // subscription page's work-group picker already uses. if (id === null) return Ungrouped; const name = workGroupNames.get(id); @@ -645,7 +645,7 @@ export function useWiredIntegrationColumns( { header: "Retry policy", cell: (row) => { - const id = setupById.get(integrationIdOf(row))?.retryPolicyId ?? null; + const id = setupById.get(subscriptionIdOf(row))?.retryPolicyId ?? null; if (id === null) return None; const name = retryPolicyNames.get(id); @@ -664,11 +664,11 @@ export function useWiredIntegrationColumns( { header: "Status", cell: (row) => { - const r = rowsById.get(integrationIdOf(row)); + const r = rowsById.get(subscriptionIdOf(row)); if (!r) return ; return ( - + ( // unbounded stack trace would push everything else out of the panel. className: "max-w-48 overflow-hidden", cell: (row) => { - const message = rowsById.get(integrationIdOf(row))?.lastException; + const message = rowsById.get(subscriptionIdOf(row))?.lastException; return message ? ( ; @@ -785,16 +785,16 @@ export function LinkListCell({ ); } -/** `LinkListCell` for the commonest case: the integrations using something. */ -export function UsedByCell({ items }: { items: IntegrationInfo[] }) { +/** `LinkListCell` for the commonest case: the subscriptions using something. */ +export function UsedByCell({ items }: { items: SubscriptionInfo[] }) { return ( ({ key: s.id, name: s.name, href: `/subscriptions/${s.id}`, - note: {INTEGRATION_TYPE_LABELS[s.type]}, + note: {SUBSCRIPTION_TYPE_LABELS[s.type]}, }))} /> ); @@ -842,12 +842,12 @@ export function TrailTable({ entries }: { entries: TrailEntry[] }) { ); } -/** Integrations referencing one particular key or value, with their type. */ -export function IntegrationMiniList({ +/** Subscriptions referencing one particular key or value, with their type. */ +export function SubscriptionMiniList({ items, emptyText, }: { - items: IntegrationInfo[]; + items: SubscriptionInfo[]; emptyText: string; }) { return ( @@ -857,7 +857,7 @@ export function IntegrationMiniList({ empty={emptyText} columns={[ { - header: "Integration", + header: "Subscription", truncate: true, cell: (s) => ( {INTEGRATION_TYPE_LABELS[s.type]}, + cell: (s) => {SUBSCRIPTION_TYPE_LABELS[s.type]}, }, ]} /> diff --git a/SW.Bitween.Web/ClientApp/src/components/mapper/useMappingEditorLoader.ts b/SW.Bitween.Web/ClientApp/src/components/mapper/useMappingEditorLoader.ts index 97ffaadb..99a2f1aa 100644 --- a/SW.Bitween.Web/ClientApp/src/components/mapper/useMappingEditorLoader.ts +++ b/SW.Bitween.Web/ClientApp/src/components/mapper/useMappingEditorLoader.ts @@ -12,8 +12,8 @@ import { recordToKvps } from "./data"; export function useMappingEditorLoader(subscriptionId: number): void { const dispatch = useMappingEditorDispatch(); const { data: subscriptionData } = useQuery({ - queryKey: ["integration", subscriptionId], - queryFn: () => api.getIntegration(subscriptionId), + queryKey: ["subscription", subscriptionId], + queryFn: () => api.getSubscription(subscriptionId), enabled: !!subscriptionId, }); const [, setLoadedForId] = useState(null); diff --git a/SW.Bitween.Web/ClientApp/src/components/mapper/useSave.ts b/SW.Bitween.Web/ClientApp/src/components/mapper/useSave.ts index 63117a16..c94839e2 100644 --- a/SW.Bitween.Web/ClientApp/src/components/mapper/useSave.ts +++ b/SW.Bitween.Web/ClientApp/src/components/mapper/useSave.ts @@ -41,11 +41,11 @@ export function useSave(subscriptionId: number): UseSaveResult { } = useMappingEditorState(); const saveMapper = useMutation({ mutationFn: (props: KeyValuePair[]) => - api.updateIntegration(subscriptionId, { + api.updateSubscription(subscriptionId, { mapperId: NATIVE_JSON_MAPPER_ID, mapperProperties: kvpsToRecord(props), }), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["integration", subscriptionId] }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["subscription", subscriptionId] }), }); const isSaving = saveMapper.isPending; const [saveSuccess, setSaveSuccess] = useState(false); diff --git a/SW.Bitween.Web/ClientApp/src/components/ui/BackLink.tsx b/SW.Bitween.Web/ClientApp/src/components/ui/BackLink.tsx index dfc08bdb..8bd6bccc 100644 --- a/SW.Bitween.Web/ClientApp/src/components/ui/BackLink.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/ui/BackLink.tsx @@ -7,7 +7,7 @@ import { ArrowLeft } from "lucide-react"; * These were all fixed `Link`s to a list page, which is only right when the list is * where you came from. `subscriptions/:id` is reached from Scheduled jobs, Exchanges, * an API gateway's page, the retry usage panel and three places on the dashboard — and - * from every one of them "← Integrations" landed you somewhere you had never been. + * from every one of them "← Subscriptions" landed you somewhere you had never been. * * So it steps back when there is somewhere to step back to, and falls back to `to` when * there isn't: a pasted link, a new tab, a refresh. The label follows the behaviour diff --git a/SW.Bitween.Web/ClientApp/src/lib/busMessageName.ts b/SW.Bitween.Web/ClientApp/src/lib/busMessageName.ts index 3e7bfe6c..7c03c243 100644 --- a/SW.Bitween.Web/ClientApp/src/lib/busMessageName.ts +++ b/SW.Bitween.Web/ClientApp/src/lib/busMessageName.ts @@ -2,7 +2,7 @@ * The one rule for a bus message name, and the one way of saying it. * * Two fields name the same thing — an information type's `busMessageTypeName` and an - * integration's `responseMessageTypeName` — and they had drifted: one silently deleted + * subscription's `responseMessageTypeName` — and they had drifted: one silently deleted * spaces as you typed, the other refused them and said why, and only one mentioned the * rule at all. Whatever the rule becomes, both fields read it from here. */ diff --git a/SW.Bitween.Web/ClientApp/src/nav.ts b/SW.Bitween.Web/ClientApp/src/nav.ts index 7012f172..aef57eaf 100644 --- a/SW.Bitween.Web/ClientApp/src/nav.ts +++ b/SW.Bitween.Web/ClientApp/src/nav.ts @@ -55,8 +55,9 @@ export const NAV_GROUPS: NavGroup[] = [ }, { // The overview first, then entry points — how a document gets in — then the - // pipelines it runs through, then who it's with. A gateway is not an integration. - label: "Integrations", + // pipelines it runs through, then who it's with. The heading names the area, not the + // entity: a gateway is not itself a subscription. + label: "Subscriptions", items: [ { label: "API gateways", path: "/api-gateways", icon: Webhook, permissions: ["api-gateways.view"] }, { label: "Bus gateways", path: "/bus-gateways", icon: Cable, permissions: ["bus-gateways.view"] }, @@ -65,7 +66,7 @@ export const NAV_GROUPS: NavGroup[] = [ // rather than a fourth kind of them. Gated on the bus alone: bus messages are what // carry work *between* gateways, so without that permission there is no flow to map. { label: "Flow map", path: "/flow", icon: Network, permissions: ["bus-gateways.view"] }, - { label: "All integrations", path: "/subscriptions", icon: Workflow, permissions: ["subscriptions.view"] }, + { label: "All subscriptions", path: "/subscriptions", icon: Workflow, permissions: ["subscriptions.view"] }, { label: "Partners", path: "/partners", icon: Handshake, permissions: ["partners.view"] }, ], }, diff --git a/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayPage.tsx index 5b4dc2cd..0f046872 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/api-gateways/ApiGatewayPage.tsx @@ -12,7 +12,7 @@ import { CopyField } from "../../components/ui/CopyField"; import { EditableTitle, Panel, UnsavedBar } from "../../components/ui/Panel"; import { MiniTable } from "../../components/ui/Table"; import { Pagination } from "../../components/ui/Pagination"; -import { useWiredIntegrationColumns } from "../../components/config/shared"; +import { useWiredSubscriptionColumns } from "../../components/config/shared"; import { BackLink } from "../../components/ui/BackLink"; const ATTACHMENTS_PAGE_SIZE = 10; @@ -24,7 +24,7 @@ export function ApiGatewayPage() { const queryClient = useQueryClient(); const [searchParams, setSearchParams] = useSearchParams(); const canEdit = useSessionCan("api-gateways.edit"); - const wiredColumns = useWiredIntegrationColumns((a) => a.integrationId); + const wiredColumns = useWiredSubscriptionColumns((a) => a.subscriptionId); const gateway = useQuery({ queryKey: ["api-gateway", gatewayId], @@ -160,7 +160,7 @@ export function ApiGatewayPage() {
{create.error?.message} diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayPage.tsx index ee6d5948..292c28d0 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewayPage.tsx @@ -2,21 +2,21 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams, useSearchParams } from "react-router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Pause, PanelLeftClose, PanelLeftOpen, Play, Trash2 } from "lucide-react"; -import { api, type BusGatewayDetail, type IntegrationDetail } from "../../api"; +import { api, type BusGatewayDetail, type SubscriptionDetail } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { Badge, Button, EmptyState, FormError, LoadingBlock } from "../../components/ui/basics"; import { ConfirmDialog, dialogsOpen } from "../../components/ui/overlays"; import { CodeBadge, EditableTitle } from "../../components/ui/Panel"; import { SearchSelect } from "../../components/ui/SearchSelect"; import { useAdapterCatalog } from "../../components/config/AdapterConfig"; -import { useIntegrationRowsById, useIntegrationsCache } from "../../components/config/shared"; -import { EMPTY_INTEGRATION, NEW_INTEGRATION_ID, draftOf } from "../integrations/studio/model"; -import { adapterIncomplete } from "../integrations/studio/faces"; +import { useSubscriptionRowsById, useSubscriptionsCache } from "../../components/config/shared"; +import { EMPTY_SUBSCRIPTION, NEW_SUBSCRIPTION_ID, draftOf } from "../subscriptions/studio/model"; +import { adapterIncomplete } from "../subscriptions/studio/faces"; import { Canvas, type Hop } from "./studio/Canvas"; import { DeliveryBody, Inspector, - IntegrationBody, + SubscriptionBody, ResponseBody, RouteBody, TransformationBody, @@ -34,7 +34,7 @@ import { routeDraftOf, routeFace, type BusNodeId, - type IntegrationDraft, + type SubscriptionDraft, type RouteDraft, } from "./studio/model"; @@ -47,10 +47,10 @@ interface RouteEdit { /** null while the route is being added — nothing to compare to yet. */ saved: RouteDraft | null; } -interface IntegrationEdit { - integrationId: number; - draft: IntegrationDraft; - saved: IntegrationDraft; +interface SubscriptionEdit { + subscriptionId: number; + draft: SubscriptionDraft; + saved: SubscriptionDraft; } /** @@ -59,9 +59,9 @@ interface IntegrationEdit { * A route is three answers — what it matches, whose values it runs with, and what * it runs — and the thing an operator actually needs to see is the fourth: what * happens after that. The table could show the first three and nothing else, so - * every question past "which integration" meant leaving the page. Here the whole + * every question past "which subscription" meant leaving the page. Here the whole * path is one diagram and every part of it is editable in place: the route, the - * integration behind it, its delivery, its response, and whoever picks that + * subscription behind it, its delivery, its response, and whoever picks that * response up. */ export function BusGatewayPage() { @@ -70,7 +70,7 @@ export function BusGatewayPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const canEdit = useSessionCan("bus-gateways.edit"); - const canEditIntegration = useSessionCan("subscriptions.edit"); + const canEditSubscription = useSessionCan("subscriptions.edit"); const [params, setParams] = useSearchParams(); const gateway = useQuery({ @@ -87,8 +87,8 @@ export function BusGatewayPage() { // Every gateway, because a response on the bus wakes routes on all of them. const allGateways = useQuery({ queryKey: ["bus-gateways"], queryFn: () => api.listBusGateways() }); const partners = useQuery({ queryKey: ["partners"], queryFn: () => api.listPartners() }); - const rowsById = useIntegrationRowsById(); - const allIntegrations = useIntegrationsCache(); + const rowsById = useSubscriptionRowsById(); + const allSubscriptions = useSubscriptionsCache(); const catalogs = { receivers: useAdapterCatalog("receiver"), validators: useAdapterCatalog("validator"), @@ -98,7 +98,7 @@ export function BusGatewayPage() { const [name, setName] = useState(null); const [routeEdit, setRouteEdit] = useState(null); - const [edit, setEdit] = useState(null); + const [edit, setEdit] = useState(null); const [collapsedInspector, setCollapsedInspector] = useState(false); const [listOpen, setListOpen] = useState(() => localStorage.getItem(LIST_KEY) !== "0"); /** undefined = closed, null = creating, number = editing that partner's values. */ @@ -158,25 +158,25 @@ export function BusGatewayPage() { // ——— the response chain, up to three hops ——— - const id0 = routeEdit?.draft.integrationId ?? null; + const id0 = routeEdit?.draft.subscriptionId ?? null; const q0 = useQuery({ - queryKey: ["integration", id0], - queryFn: () => api.getIntegration(id0!), - // The integration being defined here has no server side to fetch yet. - enabled: id0 !== null && id0 !== NEW_INTEGRATION_ID, + queryKey: ["subscription", id0], + queryFn: () => api.getSubscription(id0!), + // The subscription being defined here has no server side to fetch yet. + enabled: id0 !== null && id0 !== NEW_SUBSCRIPTION_ID, }); const d0 = useHopDraft(edit, id0, q0.data); - const id1 = d0?.responseIntegrationId ?? null; + const id1 = d0?.responseSubscriptionId ?? null; const q1 = useQuery({ - queryKey: ["integration", id1], - queryFn: () => api.getIntegration(id1!), + queryKey: ["subscription", id1], + queryFn: () => api.getSubscription(id1!), enabled: id1 !== null, }); const d1 = useHopDraft(edit, id1, q1.data); - const id2 = d1?.responseIntegrationId ?? null; + const id2 = d1?.responseSubscriptionId ?? null; const q2 = useQuery({ - queryKey: ["integration", id2], - queryFn: () => api.getIntegration(id2!), + queryKey: ["subscription", id2], + queryFn: () => api.getSubscription(id2!), enabled: id2 !== null, }); const d2 = useHopDraft(edit, id2, q2.data); @@ -185,33 +185,33 @@ export function BusGatewayPage() { { id: id0, draft: d0, data: q0.data }, { id: id1, draft: d1, data: q1.data }, { id: id2, draft: d2, data: q2.data }, - ].filter((h): h is { id: number; draft: IntegrationDraft | null; data: typeof q0.data } => h.id !== null); + ].filter((h): h is { id: number; draft: SubscriptionDraft | null; data: typeof q0.data } => h.id !== null); const activeIndex = Math.min(activeHop, Math.max(0, chain.length - 1)); const active = chain[activeIndex]; const activeData = active?.data; - // Seed the editable hop. Keyed on the integration id, so switching hop or route - // re-seeds and a draft can never be applied to the wrong integration. + // Seed the editable hop. Keyed on the subscription id, so switching hop or route + // re-seeds and a draft can never be applied to the wrong subscription. useEffect(() => { if (!active?.id) { if (edit) setEdit(null); return; } - if (edit?.integrationId === active.id) return; - if (active.id === NEW_INTEGRATION_ID) { + if (edit?.subscriptionId === active.id) return; + if (active.id === NEW_SUBSCRIPTION_ID) { // Blank, and `saved` blank too: every field the user fills counts as a change, - // so the save bar names them the same way it does for an existing integration. + // so the save bar names them the same way it does for an existing subscription. setEdit({ - integrationId: NEW_INTEGRATION_ID, - draft: structuredClone(EMPTY_INTEGRATION), - saved: structuredClone(EMPTY_INTEGRATION), + subscriptionId: NEW_SUBSCRIPTION_ID, + draft: structuredClone(EMPTY_SUBSCRIPTION), + saved: structuredClone(EMPTY_SUBSCRIPTION), }); return; } if (!activeData || activeData.id !== active.id) return; const seeded = draftOf(activeData); - setEdit({ integrationId: active.id, draft: seeded, saved: structuredClone(seeded) }); + setEdit({ subscriptionId: active.id, draft: seeded, saved: structuredClone(seeded) }); }, [active?.id, activeData, edit]); // ——— what's unsaved ——— @@ -232,17 +232,17 @@ export function BusGatewayPage() { JSON.stringify(routeEdit.draft.matchExpression) !== JSON.stringify(routeEdit.saved.matchExpression) && "filter", routeEdit.draft.partner !== routeEdit.saved.partner && "partner", - routeEdit.draft.integrationId !== routeEdit.saved.integrationId && "integration", + routeEdit.draft.subscriptionId !== routeEdit.saved.subscriptionId && "subscription", ].filter((x): x is string => typeof x === "string")); - const integrationChanges = edit + const subscriptionChanges = edit ? (Object.keys(BUS_NODES) as BusNodeId[]) - .filter((n) => OWNER[n] === "integration" && nodeDirty(n, edit.draft, edit.saved)) + .filter((n) => OWNER[n] === "subscription" && nodeDirty(n, edit.draft, edit.saved)) .map((n) => BUS_NODES[n].label.toLowerCase()) : []; const dirtyLabels = [ nameDirty && "the gateway name", isNewRoute ? "a new route" : routeIsDirty && `the route (${routeChanges.join(", ")})`, - intIsDirty && `${edit ? edit.draft.name : "the integration"} (${integrationChanges.join(", ")})`, + intIsDirty && `${edit ? edit.draft.name : "the subscription"} (${subscriptionChanges.join(", ")})`, ].filter((x): x is string => typeof x === "string"); const discard = () => { @@ -263,13 +263,13 @@ export function BusGatewayPage() { }); const selectHop = (index: number) => - guard("this integration", () => { + guard("this subscription", () => { setEdit(null); setQuery({ hop: String(index) }); }); // Escape closes the open node, but only when it holds nothing unsaved — the - // same rule the integration studio applies to its stages. + // same rule the subscription studio applies to its stages. useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key !== "Escape" || !node) return; @@ -278,7 +278,7 @@ export function BusGatewayPage() { const el = document.activeElement; if (el instanceof HTMLElement && ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName)) return; if (OWNER[node] === "route" && (routeIsDirty || isNewRoute)) return; - if (OWNER[node] === "integration" && edit && nodeDirty(node, edit.draft, edit.saved)) return; + if (OWNER[node] === "subscription" && edit && nodeDirty(node, edit.draft, edit.saved)) return; setQuery({ node: null }); }; window.addEventListener("keydown", onKey); @@ -294,32 +294,32 @@ export function BusGatewayPage() { // omitting it would reactivate a deactivated gateway on a rename. if (nameDirty && name !== null) await api.updateBusGateway(gatewayId, { name, inactive: g.inactive }); - // An integration being defined here is not written on its own: it goes with the + // A subscription being defined here is not written on its own: it goes with the // route, in the one call the endpoint commits as a single transaction, so a - // failure can't leave an integration nothing points at. - const definingIntegration = routeEdit?.draft.integrationId === NEW_INTEGRATION_ID; - const integrationDraft = edit?.draft; + // failure can't leave a subscription nothing points at. + const definingSubscription = routeEdit?.draft.subscriptionId === NEW_SUBSCRIPTION_ID; + const subscriptionDraft = edit?.draft; - // Otherwise the integration first: if the route write then fails, what was saved + // Otherwise the subscription first: if the route write then fails, what was saved // is the part that stands on its own. - if (edit && intIsDirty && !definingIntegration) - await api.updateIntegration(edit.integrationId, integrationDraft!); + if (edit && intIsDirty && !definingSubscription) + await api.updateSubscription(edit.subscriptionId, subscriptionDraft!); if (routeEdit && (isNewRoute || routeIsDirty)) { const partnerId = routeEdit.draft.partner === "none" ? null : routeEdit.draft.partner; if (isNewRoute) { const before = new Set((g?.routes ?? []).map((r) => r.id)); await api.addBusRoute(gatewayId, { - ...(definingIntegration - ? { newIntegration: integrationDraft! } - : { integrationId: routeEdit.draft.integrationId! }), + ...(definingSubscription + ? { newSubscription: subscriptionDraft! } + : { subscriptionId: routeEdit.draft.subscriptionId! }), partnerId, matchExpression: routeEdit.draft.matchExpression, }); return before; } await api.updateBusRoute(gatewayId, routeEdit.routeId as number, { - integrationId: routeEdit.draft.integrationId!, + subscriptionId: routeEdit.draft.subscriptionId!, partnerId, matchExpression: routeEdit.draft.matchExpression, }); @@ -333,12 +333,12 @@ export function BusGatewayPage() { queryKey: ["bus-gateway", gatewayId], queryFn: () => api.getBusGateway(gatewayId), }); - if (edit && edit.integrationId !== NEW_INTEGRATION_ID) - await queryClient.invalidateQueries({ queryKey: ["integration", edit.integrationId] }); + if (edit && edit.subscriptionId !== NEW_SUBSCRIPTION_ID) + await queryClient.invalidateQueries({ queryKey: ["subscription", edit.subscriptionId] }); void queryClient.invalidateQueries({ queryKey: ["bus-gateways"] }); - void queryClient.invalidateQueries({ queryKey: ["integrations"] }); - void queryClient.invalidateQueries({ queryKey: ["integration-rows"] }); - void queryClient.invalidateQueries({ queryKey: ["integration-rows-search"] }); + void queryClient.invalidateQueries({ queryKey: ["subscriptions"] }); + void queryClient.invalidateQueries({ queryKey: ["subscription-rows"] }); + void queryClient.invalidateQueries({ queryKey: ["subscription-rows-search"] }); setRouteEdit(null); setEdit(null); setName(fresh.name); @@ -366,13 +366,13 @@ export function BusGatewayPage() { : undefined; const hops: Hop[] = chain.map((h) => ({ - integrationId: h.id, + subscriptionId: h.id, name: h.draft?.name?.trim() || h.data?.name || - (h.id === NEW_INTEGRATION_ID ? "New integration" : `#${h.id}`), + (h.id === NEW_SUBSCRIPTION_ID ? "New subscription" : `#${h.id}`), draft: h.draft, - saved: edit?.integrationId === h.id ? edit.saved : h.draft, + saved: edit?.subscriptionId === h.id ? edit.saved : h.draft, row: rowsById.get(h.id), destination: h.draft?.responseMessageTypeName && informationTypes.data @@ -381,25 +381,25 @@ export function BusGatewayPage() { })); // What still blocks a save, said the way the modal used to say it at its Create - // button. The rule outlives the modal: an integration defined here cannot be saved + // button. The rule outlives the modal: a subscription defined here cannot be saved // half-made, and the server refuses it too. const missing = [ - ...(routeEdit?.draft.integrationId === NEW_INTEGRATION_ID && edit + ...(routeEdit?.draft.subscriptionId === NEW_SUBSCRIPTION_ID && edit ? [ edit.draft.name.trim().length < 2 && "a name", !edit.draft.handlerId && "a delivery", adapterIncomplete(catalogs.handlers, edit.draft.handlerId, edit.draft.handlerProperties) && "its required delivery fields", ] - : isNewRoute && routeEdit?.draft.integrationId === null - ? ["an integration"] + : isNewRoute && routeEdit?.draft.subscriptionId === null + ? ["a subscription"] : []), ].filter((m): m is string => typeof m === "string"); const nodeIsDirty = node ? OWNER[node] === "route" ? routeIsDirty || isNewRoute - : OWNER[node] === "integration" && !!edit && nodeDirty(node, edit.draft, edit.saved) + : OWNER[node] === "subscription" && !!edit && nodeDirty(node, edit.draft, edit.saved) : false; const renderNode = () => { @@ -416,13 +416,13 @@ export function BusGatewayPage() { disabled={!canEdit} onNewPartner={() => setPartnerDialog(null)} onEditPartner={(id) => setPartnerDialog(id)} - onNewIntegration={() => { - // No modal: the route draft points at the integration being defined, and the + onNewSubscription={() => { + // No modal: the route draft points at the subscription being defined, and the // canvas draws it like any other. Straight to its own node, where the name is. setRouteEdit((r) => - r ? { ...r, draft: { ...r.draft, integrationId: NEW_INTEGRATION_ID } } : r, + r ? { ...r, draft: { ...r.draft, subscriptionId: NEW_SUBSCRIPTION_ID } } : r, ); - setQuery({ node: "integration" }); + setQuery({ node: "subscription" }); }} /> ) @@ -430,27 +430,27 @@ export function BusGatewayPage() { if (!edit) return (

- {routeEdit?.draft.integrationId === null - ? "Pick the integration this route runs, or define one — this step belongs to it." - : "Loading the integration…"} + {routeEdit?.draft.subscriptionId === null + ? "Pick the subscription this route runs, or define one — this step belongs to it." + : "Loading the subscription…"}

); - const onChange = (patch: Partial) => + const onChange = (patch: Partial) => setEdit((e) => (e ? { ...e, draft: { ...e.draft, ...patch } } : e)); switch (node) { - case "integration": + case "subscription": return ( - ); case "transformation": @@ -458,23 +458,23 @@ export function BusGatewayPage() { ); case "delivery": - return ; + return ; case "response": return ( x.id !== edit.integrationId)} + disabled={!canEditSubscription} + candidates={(allSubscriptions.data ?? []).filter((x) => x.id !== edit.subscriptionId)} /> ); } @@ -541,7 +541,7 @@ export function BusGatewayPage() { onChange={(v) => v !== "" && select(Number(v))} options={g.routes.map((r) => ({ value: String(r.id), - label: r.integrationName || `#${r.integrationId}`, + label: r.subscriptionName || `#${r.subscriptionId}`, code: r.partnerName ?? undefined, hint: r.partnerName ?? "Any partner", }))} @@ -618,13 +618,13 @@ export function BusGatewayPage() { selectedNode={node} onSelectNode={(next) => setQuery({ node: next })} catalogs={catalogs} - integrationNames={allIntegrations.data ?? []} + subscriptionNames={allSubscriptions.data ?? []} onOpenListener={(l) => l.gatewayId === gatewayId ? select(l.routeId) : guard("this route", () => navigate(`/bus-gateways/${l.gatewayId}?route=${l.routeId}`)) } - routeChosen={routeEdit.draft.integrationId !== null} + routeChosen={routeEdit.draft.subscriptionId !== null} gutterRem={listOpen ? 21 : 0} resetKey={`${selection}`} /> @@ -640,7 +640,7 @@ export function BusGatewayPage() { {renderNode()} - {dirty && (canEdit || canEditIntegration) && ( + {dirty && (canEdit || canEditSubscription) && (

@@ -714,9 +714,9 @@ export function BusGatewayPage() { <> Messages matching it stop reaching{" "} - {g.routes.find((r) => r.id === removingRoute)?.integrationName} + {g.routes.find((r) => r.id === removingRoute)?.subscriptionName} - . The integration itself is kept. + . The subscription itself is kept. } confirmLabel="Remove route" @@ -727,7 +727,7 @@ export function BusGatewayPage() { queryFn: () => api.getBusGateway(gatewayId), }); void queryClient.invalidateQueries({ queryKey: ["bus-gateways"] }); - void queryClient.invalidateQueries({ queryKey: ["integrations"] }); + void queryClient.invalidateQueries({ queryKey: ["subscriptions"] }); setRouteEdit(null); setEdit(null); setQuery({ route: fresh.routes[0] ? String(fresh.routes[0].id) : null, hop: null }); @@ -760,14 +760,14 @@ export function BusGatewayPage() { body={ <> {g.name} stops listening and all its - routes are removed. The integrations behind them are kept. + routes are removed. The subscriptions behind them are kept. } confirmLabel="Delete gateway" onConfirm={async () => { await api.deleteBusGateway(gatewayId); void queryClient.invalidateQueries({ queryKey: ["bus-gateways"] }); - void queryClient.invalidateQueries({ queryKey: ["integrations"] }); + void queryClient.invalidateQueries({ queryKey: ["subscriptions"] }); navigate("/bus-gateways"); }} onClose={() => setDeletingGateway(false)} @@ -782,18 +782,18 @@ export function BusGatewayPage() { * saved record otherwise. Taking the draft is what makes picking a response * target grow the chain on the canvas before anything is saved. * - * Matched on the integration's own id rather than on which hop is active — a + * Matched on the subscription's own id rather than on which hop is active — a * stale `?hop=` in the URL would otherwise leave the edited node reading from * saved data while the draft went nowhere. */ function useHopDraft( - edit: IntegrationEdit | null, - integrationId: number | null, - data: IntegrationDetail | undefined, -): IntegrationDraft | null { + edit: SubscriptionEdit | null, + subscriptionId: number | null, + data: SubscriptionDetail | undefined, +): SubscriptionDraft | null { return useMemo(() => { - if (integrationId === null) return null; - if (edit?.integrationId === integrationId) return edit.draft; - return data && data.id === integrationId ? draftOf(data) : null; - }, [edit, integrationId, data]); + if (subscriptionId === null) return null; + if (edit?.subscriptionId === subscriptionId) return edit.draft; + return data && data.id === subscriptionId ? draftOf(data) : null; + }, [edit, subscriptionId, data]); } diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewaysPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewaysPage.tsx index dd3aaf1c..4dc442d3 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewaysPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/BusGatewaysPage.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { Link, useNavigate, useSearchParams } from "react-router"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Cable, Network, Plus, Search } from "lucide-react"; -import { api, type BusGatewayRow, type IntegrationRow } from "../../api"; +import { api, type BusGatewayRow, type SubscriptionRow } from "../../api"; import { Can, useSessionCan } from "../../auth/guards"; import { PageHeader } from "../../components/layout/PageHeader"; import { Badge, Button, EmptyState, LoadingBlock } from "../../components/ui/basics"; @@ -13,13 +13,13 @@ import { Table } from "../../components/ui/Table"; import { LinkListCell, WiredHealthBadge, - useIntegrationRowsById, + useSubscriptionRowsById, } from "../../components/config/shared"; import { matchSummary } from "../../lib/match"; /** * Bus gateways — messages picked off the bus. A gateway listens for one - * information type; its routes decide which integration handles which message. + * information type; its routes decide which subscription handles which message. */ const PAGE_SIZE = 25; @@ -46,7 +46,7 @@ export function BusGatewaysPage() { queryFn: () => api.searchBusGateways({ search: q, informationTypeId, inactive, offset, limit: PAGE_SIZE }), placeholderData: keepPreviousData, }); - const integrationsById = useIntegrationRowsById(); + const subscriptionsById = useSubscriptionRowsById(); const infoTypes = useQuery({ queryKey: ["information-types"], @@ -74,7 +74,7 @@ export function BusGatewaysPage() {

{/* Offered here because this is where someone asking "what feeds what?" @@ -235,11 +235,11 @@ export function BusGatewaysPage() { truncate: true, cell: (g) => ( ({ key: r.id, - name: r.integrationName, - href: `/subscriptions/${r.integrationId}`, + name: r.subscriptionName, + href: `/subscriptions/${r.subscriptionId}`, note: {matchSummary(r.matchExpression)}, }))} /> @@ -261,8 +261,8 @@ export function BusGatewaysPage() { integrationsById.get(r.integrationId)) - .filter((r): r is IntegrationRow => Boolean(r))} + .map((r) => subscriptionsById.get(r.subscriptionId)) + .filter((r): r is SubscriptionRow => Boolean(r))} /> ); }, diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Canvas.tsx b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Canvas.tsx index 835cfdfe..6a974089 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Canvas.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/Canvas.tsx @@ -1,11 +1,11 @@ import { Fragment } from "react"; import { Link } from "react-router"; import { ArrowUpRight, Radio, Workflow } from "lucide-react"; -import type { IntegrationRow } from "../../../api"; +import type { SubscriptionRow } from "../../../api"; import { PanZoomCanvas } from "../../../components/ui/PanZoomCanvas"; -import { Connector, StageNode, type StageFace } from "../../integrations/studio/StageRail"; -import { faceOf, type AdapterCatalogs } from "../../integrations/studio/faces"; -import { NEW_INTEGRATION_ID } from "../../integrations/studio/model"; +import { Connector, StageNode, type StageFace } from "../../subscriptions/studio/StageRail"; +import { faceOf, type AdapterCatalogs } from "../../subscriptions/studio/faces"; +import { NEW_SUBSCRIPTION_ID } from "../../subscriptions/studio/model"; import { BUS_NODES, HEALTH_DOT, @@ -16,21 +16,21 @@ import { type BusDestination, type BusListener, type BusNodeId, - type IntegrationDraft, + type SubscriptionDraft, } from "./model"; /** - * One integration in the response chain. Only `active` is expanded into editable + * One subscription in the response chain. Only `active` is expanded into editable * nodes; the rest are summaries — three hops fully expanded is fifteen nodes, and * nobody reads fifteen nodes. */ export interface Hop { - integrationId: number; + subscriptionId: number; name: string; - /** Present once the integration's detail has loaded. */ - draft: IntegrationDraft | null; - saved: IntegrationDraft | null; - row: IntegrationRow | undefined; + /** Present once the subscription's detail has loaded. */ + draft: SubscriptionDraft | null; + saved: SubscriptionDraft | null; + row: SubscriptionRow | undefined; /** Set on the response message this hop publishes, if any. */ destination: BusDestination | null; } @@ -50,7 +50,7 @@ export function Canvas({ selectedNode, onSelectNode, catalogs, - integrationNames, + subscriptionNames, onOpenListener, routeChosen, /** Room reserved on the left for the floating route panel, in rem. */ @@ -66,9 +66,9 @@ export function Canvas({ selectedNode: BusNodeId | null; onSelectNode: (node: BusNodeId | null) => void; catalogs: AdapterCatalogs; - integrationNames: { id: number; name: string }[]; + subscriptionNames: { id: number; name: string }[]; onOpenListener: (listener: BusListener) => void; - /** False while a new route still has no integration — there is no chain to draw yet. */ + /** False while a new route still has no subscription — there is no chain to draw yet. */ routeChosen: boolean; gutterRem: number; resetKey: string; @@ -100,7 +100,7 @@ export function Canvas({

- Pick the integration this route runs — it appears here, with its whole pipeline. + Pick the subscription this route runs — it appears here, with its whole pipeline.

@@ -113,7 +113,7 @@ export function Canvas({ selectedNode={selectedNode} onSelectNode={pick} catalogs={catalogs} - integrationNames={integrationNames} + subscriptionNames={subscriptionNames} onOpenListener={onOpenListener} /> )} @@ -137,9 +137,9 @@ export function Canvas({ * One hop and everything downstream of it. * * Recursive, and forked rather than flattened into a single row: a Response can - * both chain into another integration *and* publish on the bus, and those two are + * both chain into another subscription *and* publish on the bus, and those two are * siblings. Drawn one after the other, the bus card read as if it fed the chained - * integration, which is a different — and wrong — story about the data path. + * subscription, which is a different — and wrong — story about the data path. */ function HopChain({ hops, @@ -149,7 +149,7 @@ function HopChain({ selectedNode, onSelectNode, catalogs, - integrationNames, + subscriptionNames, onOpenListener, }: { hops: Hop[]; @@ -159,7 +159,7 @@ function HopChain({ selectedNode: BusNodeId | null; onSelectNode: (node: BusNodeId) => void; catalogs: AdapterCatalogs; - integrationNames: { id: number; name: string }[]; + subscriptionNames: { id: number; name: string }[]; onOpenListener: (listener: BusListener) => void; }) { const hop = hops[index]; @@ -167,7 +167,7 @@ function HopChain({ const hasNext = index + 1 < hops.length; // Depth cap: three hops is already more chain than a real gateway has, and a // fourth would push the route itself off the screen. - const cappedAt = !hasNext && hop.draft?.responseIntegrationId != null; + const cappedAt = !hasNext && hop.draft?.responseSubscriptionId != null; const forks = hasNext || cappedAt || !!hop.destination; return ( @@ -179,14 +179,14 @@ function HopChain({ selectedNode={selectedNode} onSelectNode={onSelectNode} catalogs={catalogs} - integrationNames={integrationNames} + subscriptionNames={subscriptionNames} /> ) : ( onSelectHop(index)} /> )} @@ -203,7 +203,7 @@ function HopChain({ selectedNode={selectedNode} onSelectNode={onSelectNode} catalogs={catalogs} - integrationNames={integrationNames} + subscriptionNames={subscriptionNames} onOpenListener={onOpenListener} />
@@ -212,12 +212,12 @@ function HopChain({
The chain continues - Open the next integration + Open the next subscription
@@ -238,23 +238,23 @@ function HopChain({ ); } -/** The active integration: its own pipeline, editable, inside one labelled frame. */ +/** The active subscription: its own pipeline, editable, inside one labelled frame. */ function ExpandedHop({ hop, selectedNode, onSelectNode, catalogs, - integrationNames, + subscriptionNames, }: { hop: Hop; selectedNode: BusNodeId | null; onSelectNode: (node: BusNodeId) => void; catalogs: AdapterCatalogs; - integrationNames: { id: number; name: string }[]; + subscriptionNames: { id: number; name: string }[]; }) { const health = routeHealth(hop.row); - const isNew = hop.integrationId === NEW_INTEGRATION_ID; - const headerDirty = hop.draft && hop.saved ? nodeDirty("integration", hop.draft, hop.saved) : false; + const isNew = hop.subscriptionId === NEW_SUBSCRIPTION_ID; + const headerDirty = hop.draft && hop.saved ? nodeDirty("subscription", hop.draft, hop.saved) : false; if (!hop.draft) return ( @@ -263,7 +263,7 @@ function ExpandedHop({
); - // Typed as the integration studio's own stage ids: these three nodes *are* its + // Typed as the subscription studio's own stage ids: these three nodes *are* its // stages, and `faceOf` is what describes them. const stages = ["transformation", "delivery", "response"] as const; @@ -271,10 +271,10 @@ function ExpandedHop({
@@ -206,18 +206,18 @@ export function RouteBody({ ); } -/** Integration-level facts: name, whether it runs, and the lane and policy it runs under. */ -export function IntegrationBody({ +/** Subscription-level facts: name, whether it runs, and the lane and policy it runs under. */ +export function SubscriptionBody({ draft, onChange, disabled, health, lastException, - /** Set while the integration is being defined here — the name is the first thing asked. */ + /** Set while the subscription is being defined here — the name is the first thing asked. */ autoFocusName = false, }: { - draft: IntegrationDraft; - onChange: (patch: Partial) => void; + draft: SubscriptionDraft; + onChange: (patch: Partial) => void; disabled: boolean; health: { isRunning: boolean; consecutiveFailures: number } | null; lastException: string | null; @@ -297,8 +297,8 @@ export function TransformationBody({ disabled, mapperEditorHref, }: { - draft: IntegrationDraft; - onChange: (patch: Partial) => void; + draft: SubscriptionDraft; + onChange: (patch: Partial) => void; disabled: boolean; mapperEditorHref?: string | null; }) { @@ -320,8 +320,8 @@ export function DeliveryBody({ onChange, disabled, }: { - draft: IntegrationDraft; - onChange: (patch: Partial) => void; + draft: SubscriptionDraft; + onChange: (patch: Partial) => void; disabled: boolean; }) { return ( @@ -342,16 +342,16 @@ export function ResponseBody({ disabled, candidates, }: { - draft: IntegrationDraft; - onChange: (patch: Partial) => void; + draft: SubscriptionDraft; + onChange: (patch: Partial) => void; disabled: boolean; - candidates: { id: number; name: string; type: IntegrationType }[]; + candidates: { id: number; name: string; type: SubscriptionType }[]; }) { return (
; + rowsById: Map; selected: Selection; onSelect: (selection: Selection) => void; onAdd: () => void; @@ -43,11 +43,11 @@ export function RouteList({ const rows = useMemo(() => { const decorated = routes.map((r) => { - const row = rowsById.get(r.integrationId); + const row = rowsById.get(r.subscriptionId); return { route: r, condition: conditionText(r.matchExpression), - integrationName: r.integrationName || `#${r.integrationId}`, + subscriptionName: r.subscriptionName || `#${r.subscriptionId}`, partner: r.partnerName ?? "Any partner", health: routeHealth(row), }; @@ -55,7 +55,7 @@ export function RouteList({ const needle = query.trim().toLowerCase(); if (!needle) return decorated; return decorated.filter((d) => - [d.condition, d.integrationName, d.partner].some((f) => f.toLowerCase().includes(needle)), + [d.condition, d.subscriptionName, d.partner].some((f) => f.toLowerCase().includes(needle)), ); }, [routes, rowsById, query]); @@ -93,7 +93,7 @@ export function RouteList({ onSelect={() => onSelect("new")} condition={conditionText(pending.matchExpression)} partner={pending.partner === "none" ? "Any partner" : "…"} - integrationName="New route — not saved" + subscriptionName="New route — not saved" health="unknown" dirty /> @@ -106,7 +106,7 @@ export function RouteList({ onSelect={() => onSelect(d.route.id)} condition={d.condition} partner={d.partner} - integrationName={d.integrationName} + subscriptionName={d.subscriptionName} health={d.health} dirty={dirtyRouteId === d.route.id} /> @@ -129,7 +129,7 @@ function Row({ onSelect, condition, partner, - integrationName, + subscriptionName, health, dirty, }: { @@ -137,7 +137,7 @@ function Row({ onSelect: () => void; condition: string; partner: string; - integrationName: string; + subscriptionName: string; health: keyof typeof HEALTH_DOT; dirty: boolean; }) { @@ -178,8 +178,8 @@ function Row({ - - {integrationName} + + {subscriptionName} diff --git a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/model.ts b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/model.ts index 93605569..2bb5cff4 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/model.ts +++ b/SW.Bitween.Web/ClientApp/src/pages/bus-gateways/studio/model.ts @@ -3,29 +3,29 @@ import type { BusGatewayRoute, BusGatewayRow, InformationTypeRow, - IntegrationRow, + SubscriptionRow, MatchGroup, } from "../../../api"; import { matchSummary } from "../../../lib/match"; -import type { StageFace } from "../../integrations/studio/StageRail"; -import { STAGES, type StageId } from "../../integrations/studio/stages"; -import { stageDirty, type Draft as IntegrationDraft } from "../../integrations/studio/model"; +import type { StageFace } from "../../subscriptions/studio/StageRail"; +import { STAGES, type StageId } from "../../subscriptions/studio/stages"; +import { stageDirty, type Draft as SubscriptionDraft } from "../../subscriptions/studio/model"; -export type { IntegrationDraft }; +export type { SubscriptionDraft }; /** - * The studio's nodes. Three of them are an integration's own pipeline stages and - * keep their names from the integration studio, because they are the same thing - * — a route that opened "Delivery" and an integration page that opened + * The studio's nodes. Three of them are a subscription's own pipeline stages and + * keep their names from the subscription studio, because they are the same thing + * — a route that opened "Delivery" and a subscription page that opened * "Delivery" have to be editing the same field. * * `validation` is absent on purpose: `RunValidator` is only reached from the * partner-key API path, so a validator on a bus-gateway subscription is stored - * and never run. See the note in the integration studio's `stages.ts`. + * and never run. See the note in the subscription studio's `stages.ts`. */ export type BusNodeId = | "route" - | "integration" + | "subscription" | Extract; export const BUS_NODES: Record = { @@ -34,8 +34,8 @@ export const BUS_NODES: Record = { +export const OWNER: Record = { route: "route", - integration: "integration", - transformation: "integration", - delivery: "integration", - response: "integration", + subscription: "subscription", + transformation: "subscription", + delivery: "subscription", + response: "subscription", }; /** - * Which integration fields each node owns, so a node can carry an unsaved dot. - * The three pipeline nodes defer to the integration studio's own map — one list + * Which subscription fields each node owns, so a node can carry an unsaved dot. + * The three pipeline nodes defer to the subscription studio's own map — one list * per field, wherever the field is edited. */ -export const INTEGRATION_NODE_FIELDS: Partial> = { - integration: ["name", "enabled", "workGroupId", "retryPolicyId"], +export const SUBSCRIPTION_NODE_FIELDS: Partial> = { + subscription: ["name", "enabled", "workGroupId", "retryPolicyId"], }; export const nodeDirty = ( node: BusNodeId, - draft: IntegrationDraft, - saved: IntegrationDraft, + draft: SubscriptionDraft, + saved: SubscriptionDraft, ): boolean => { - const own = INTEGRATION_NODE_FIELDS[node]; + const own = SUBSCRIPTION_NODE_FIELDS[node]; if (own) return own.some((f) => JSON.stringify(draft[f]) !== JSON.stringify(saved[f])); if (node === "transformation" || node === "delivery" || node === "response") return stageDirty(node, draft, saved); @@ -78,13 +78,13 @@ export const nodeDirty = ( export interface RouteDraft { matchExpression: MatchGroup | null; partner: number | "none"; - integrationId: number | null; + subscriptionId: number | null; } export const routeDraftOf = (r: BusGatewayRoute): RouteDraft => ({ matchExpression: structuredClone(r.matchExpression), partner: r.partnerId ?? "none", - integrationId: r.integrationId, + subscriptionId: r.subscriptionId, }); /** @@ -92,7 +92,7 @@ export const routeDraftOf = (r: BusGatewayRoute): RouteDraft => ({ * what lets the canvas draw it exactly like a real one — a modal asking the same * three questions would have hidden the diagram the answers are about. */ -export const NEW_ROUTE: RouteDraft = { matchExpression: null, partner: "none", integrationId: null }; +export const NEW_ROUTE: RouteDraft = { matchExpression: null, partner: "none", subscriptionId: null }; export const routeDirty = (draft: RouteDraft, saved: RouteDraft): boolean => JSON.stringify(draft) !== JSON.stringify(saved); @@ -133,8 +133,8 @@ export interface BusListener { gatewayId: number; gatewayName: string; routeId: number; - integrationId: number; - integrationName: string; + subscriptionId: number; + subscriptionName: string; condition: string; } @@ -159,18 +159,18 @@ export function busDestination( gatewayId: g.id, gatewayName: g.name, routeId: r.id, - integrationId: r.integrationId, - integrationName: r.integrationName, + subscriptionId: r.subscriptionId, + subscriptionName: r.subscriptionName, condition: conditionText(r.matchExpression), })), ); return { informationType, listeners }; } -/** Health of the integration behind a route, for the list and the rolled-up chain cards. */ +/** Health of the subscription behind a route, for the list and the rolled-up chain cards. */ export type RouteHealth = "ok" | "failing" | "disabled" | "paused" | "unknown"; -export function routeHealth(row: IntegrationRow | undefined): RouteHealth { +export function routeHealth(row: SubscriptionRow | undefined): RouteHealth { if (!row) return "unknown"; if (!row.enabled) return "disabled"; if (row.paused) return "paused"; @@ -198,7 +198,7 @@ export const HEALTH_DOT: Record = { export const HEALTH_TITLE: Record = { ok: "Healthy — enabled, unpaused, and its last run succeeded.", failing: "Enabled and unpaused, but its recent runs ended in errors.", - disabled: "Turned off — this route's integration won't run, even if the filter matches.", + disabled: "Turned off — this route's subscription won't run, even if the filter matches.", paused: "Held without being disabled — matches still route here, but nothing runs until unpaused.", - unknown: "No integration data available for this route yet.", + unknown: "No subscription data available for this route yet.", }; diff --git a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx index ea249d21..e15c125f 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/dashboard/DashboardPage.tsx @@ -62,7 +62,7 @@ export function DashboardPage() { const delta = data.today.total - data.yesterdayTotal; const maxDay = Math.max(1, ...data.trafficByDay.map((d) => d.success + d.failed)); const needsAttention = - data.attention.failingIntegrations.length + data.attention.pausedIntegrations.length; + data.attention.failingSubscriptions.length + data.attention.pausedSubscriptions.length; return (
@@ -211,12 +211,12 @@ export function DashboardPage() {
- {f.integrationName && ( + {f.subscriptionName && ( - {f.integrationName} + {f.subscriptionName} )} {timeAgo(f.on)} @@ -238,13 +238,13 @@ export function DashboardPage() { - {/* — integration health — */} - + {/* — subscription health — */} + {needsAttention === 0 ? ( - No failures piling up, nothing paused. + No failures piling up, nothing paused. ) : (
    - {data.attention.failingIntegrations.map((s) => ( + {data.attention.failingSubscriptions.map((s) => (
  • ))} - {data.attention.pausedIntegrations.map((s) => ( + {data.attention.pausedSubscriptions.map((s) => (
- {/* — busiest integrations — */} - + {/* — busiest subscriptions — */} + {data.busiest.length === 0 ? ( Once exchanges flow, the busiest pipelines rank here. ) : ( diff --git a/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangeNewPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangeNewPage.tsx index 145926e7..45d72789 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangeNewPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangeNewPage.tsx @@ -6,28 +6,28 @@ import { PageHeader } from "../../components/layout/PageHeader"; import { Button, FormError } from "../../components/ui/basics"; import { Field } from "../../components/ui/forms"; import { SearchSelect } from "../../components/ui/SearchSelect"; -import { useIntegrationsCache } from "../../components/config/shared"; +import { useSubscriptionsCache } from "../../components/config/shared"; /** * Manually inject a payload — useful for testing a pipeline without waiting - * for real traffic. Addressed either at one integration, or at an - * information type (every matching integration picks it up). + * for real traffic. Addressed either at one subscription, or at an + * information type (every matching subscription picks it up). */ export function ExchangeNewPage() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); - const target = searchParams.get("target") === "informationType" ? "informationType" : "integration"; - const setTarget = (t: "integration" | "informationType") => { + const target = searchParams.get("target") === "informationType" ? "informationType" : "subscription"; + const setTarget = (t: "subscription" | "informationType") => { const next = new URLSearchParams(searchParams); next.set("target", t); setSearchParams(next, { replace: true }); }; - const [integrationId, setIntegrationId] = useState(""); + const [subscriptionId, setSubscriptionId] = useState(""); const [informationTypeId, setInformationTypeId] = useState(""); const [data, setData] = useState(""); const [error, setError] = useState(null); - const integrations = useIntegrationsCache().data ?? []; + const subscriptions = useSubscriptionsCache().data ?? []; const infoTypes = useQuery({ queryKey: ["information-types"], queryFn: () => api.listInformationTypes() }).data ?? []; @@ -35,7 +35,7 @@ export function ExchangeNewPage() { mutationFn: () => api.createExchange({ target, - integrationId: integrationId ? Number(integrationId) : undefined, + subscriptionId: subscriptionId ? Number(subscriptionId) : undefined, informationTypeId: informationTypeId ? Number(informationTypeId) : undefined, data, }), @@ -46,7 +46,7 @@ export function ExchangeNewPage() { const submit = () => { setError(null); - if (target === "integration" && !integrationId) return setError("Pick the integration to run."); + if (target === "subscription" && !subscriptionId) return setError("Pick the subscription to run."); if (target === "informationType" && !informationTypeId) return setError("Pick the information type to send."); if (!data.trim()) return setError("Paste the payload the exchange should carry."); @@ -64,7 +64,7 @@ export function ExchangeNewPage() {
{( [ - { id: "integration", label: "Send to an integration" }, + { id: "subscription", label: "Send to a subscription" }, { id: "informationType", label: "Send as an information type" }, ] as const ).map((t) => ( @@ -83,22 +83,22 @@ export function ExchangeNewPage() { ))}
- {target === "integration" ? ( + {target === "subscription" ? ( ({ value: String(i.id), label: i.name }))} + value={subscriptionId} + onChange={setSubscriptionId} + placeholder="Pick a subscription…" + options={subscriptions.map((i) => ({ value: String(i.id), label: i.name }))} /> ) : ( ({ status: (sp.get("status") as ExchangeStatus | null) ?? undefined, - integrationId: sp.get("integrationId") ? Number(sp.get("integrationId")) : undefined, + subscriptionId: sp.get("subscriptionId") ? Number(sp.get("subscriptionId")) : undefined, partnerId: sp.get("partnerId") ? Number(sp.get("partnerId")) : undefined, informationTypeId: sp.get("informationTypeId") ? Number(sp.get("informationTypeId")) : undefined, ids: sp.get("ids") ?? undefined, @@ -59,7 +59,7 @@ export function ExchangesPage() { placeholderData: keepPreviousData, }); - const integrations = useIntegrationsCache().data ?? []; + const subscriptions = useSubscriptionsCache().data ?? []; const partners = useQuery({ queryKey: ["partners"], queryFn: () => api.listPartners() }).data ?? []; const infoTypes = useQuery({ queryKey: ["information-types"], queryFn: () => api.listInformationTypes() }).data ?? []; @@ -193,12 +193,12 @@ export function ExchangesPage() { {/* — filters — */}
setParam("integrationId", v || null)} - options={integrations.map((i) => ({ value: String(i.id), label: i.name }))} + clearLabel="Any subscription" + value={query.subscriptionId?.toString() ?? ""} + onChange={(v) => setParam("subscriptionId", v || null)} + options={subscriptions.map((i) => ({ value: String(i.id), label: i.name }))} /> {activeFilterCount > 0 ? "Try removing some filters — or widen the date range." - : "Traffic will show up here as soon as an integration processes something."} + : "Traffic will show up here as soon as a subscription processes something."} ) : (
@@ -330,7 +330,7 @@ export function ExchangesPage() { Journey Information type Partner - Integration + Subscription Started @@ -401,13 +401,13 @@ export function ExchangesPage() { )} - {x.integrationName ? ( + {x.subscriptionName ? ( e.stopPropagation()} className="font-medium text-ink-800 hover:text-crimson-700 hover:underline" > - {x.integrationName} + {x.subscriptionName} ) : ( diff --git a/SW.Bitween.Web/ClientApp/src/pages/exchanges/shared.tsx b/SW.Bitween.Web/ClientApp/src/pages/exchanges/shared.tsx index 8a402e0b..57559fb6 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/exchanges/shared.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/exchanges/shared.tsx @@ -140,7 +140,7 @@ export function RetryDialog({

setReset(e.target.checked)} /> diff --git a/SW.Bitween.Web/ClientApp/src/pages/flow/FlowMap.tsx b/SW.Bitween.Web/ClientApp/src/pages/flow/FlowMap.tsx index 6902cdf4..bc590af5 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/flow/FlowMap.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/flow/FlowMap.tsx @@ -9,7 +9,7 @@ const KINDS: Record = { busGateway: { label: "Bus gateway", icon: Radio }, apiGateway: { label: "API gateway", icon: Globe }, job: { label: "Scheduled job", icon: Clock }, - integration: { label: "Integration", icon: Workflow }, + subscription: { label: "Subscription", icon: Workflow }, }; /** diff --git a/SW.Bitween.Web/ClientApp/src/pages/flow/FlowPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/flow/FlowPage.tsx index 4106c873..58b9befc 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/flow/FlowPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/flow/FlowPage.tsx @@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query"; import { AlertTriangle, ChevronDown, RotateCcw } from "lucide-react"; import { api } from "../../api"; import { useSessionCan } from "../../auth/guards"; -import { useIntegrationsCache } from "../../components/config/shared"; +import { useSubscriptionsCache } from "../../components/config/shared"; import { EmptyState, LoadingBlock } from "../../components/ui/basics"; import { FlowLegend, FlowMap } from "./FlowMap"; import { buildFlowGraph } from "./model"; @@ -25,7 +25,7 @@ export function FlowPage() { const canSeeApi = useSessionCan("api-gateways.view"); const canSeeBus = useSessionCan("bus-gateways.view"); - const integrations = useIntegrationsCache(); + const subscriptions = useSubscriptionsCache(); const informationTypes = useQuery({ queryKey: ["information-types"], queryFn: () => api.listInformationTypes(), @@ -47,9 +47,9 @@ export function FlowPage() { apiGateways: apiGateways.data ?? [], busGateways: busGateways.data ?? [], informationTypes: informationTypes.data ?? [], - integrations: integrations.data ?? [], + subscriptions: subscriptions.data ?? [], }), - [apiGateways.data, busGateways.data, informationTypes.data, integrations.data], + [apiGateways.data, busGateways.data, informationTypes.data, subscriptions.data], ); // Laid out here rather than inside the map: the page's own loop banner needs to @@ -57,7 +57,7 @@ export function FlowPage() { const layout = useMemo(() => layoutFlow(graph), [graph]); const pending = - integrations.isPending || + subscriptions.isPending || informationTypes.isPending || (canSeeApi && apiGateways.isPending) || (canSeeBus && busGateways.isPending); diff --git a/SW.Bitween.Web/ClientApp/src/pages/flow/layout.ts b/SW.Bitween.Web/ClientApp/src/pages/flow/layout.ts index 95ec1c9f..2d050ed3 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/flow/layout.ts +++ b/SW.Bitween.Web/ClientApp/src/pages/flow/layout.ts @@ -44,7 +44,7 @@ const KIND_ORDER: Record = { busGateway: 1, apiGateway: 2, job: 3, - integration: 4, + subscription: 4, }; /** diff --git a/SW.Bitween.Web/ClientApp/src/pages/flow/model.ts b/SW.Bitween.Web/ClientApp/src/pages/flow/model.ts index a72d4c6d..62b11fc1 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/flow/model.ts +++ b/SW.Bitween.Web/ClientApp/src/pages/flow/model.ts @@ -1,4 +1,4 @@ -import type { ApiGatewayRow, BusGatewayRow, InformationTypeRow, IntegrationInfo } from "../../api"; +import type { ApiGatewayRow, BusGatewayRow, InformationTypeRow, SubscriptionInfo } from "../../api"; /** * The bus topology as a graph. @@ -17,7 +17,7 @@ import type { ApiGatewayRow, BusGatewayRow, InformationTypeRow, IntegrationInfo * response published to nobody at all. */ -export type FlowNodeKind = "apiGateway" | "busGateway" | "job" | "integration" | "message"; +export type FlowNodeKind = "apiGateway" | "busGateway" | "job" | "subscription" | "message"; export interface FlowNode { id: string; @@ -47,7 +47,7 @@ export interface FlowEdge { export interface FlowGraph { nodes: FlowNode[]; edges: FlowEdge[]; - /** Integrations with no place on the map: no gateway runs them, no response leaves them. */ + /** Subscriptions with no place on the map: no gateway runs them, no response leaves them. */ omitted: number; } @@ -60,12 +60,12 @@ export function buildFlowGraph({ apiGateways, busGateways, informationTypes, - integrations, + subscriptions, }: { apiGateways: ApiGatewayRow[]; busGateways: BusGatewayRow[]; informationTypes: InformationTypeRow[]; - integrations: IntegrationInfo[]; + subscriptions: SubscriptionInfo[]; }): FlowGraph { const nodes = new Map(); const edges: FlowEdge[] = []; @@ -74,10 +74,10 @@ export function buildFlowGraph({ if (node) node.warning = node.warning ? `${node.warning} ${text}` : text; }; - const integrationById = new Map(integrations.map((i) => [i.id, i])); + const subscriptionById = new Map(subscriptions.map((i) => [i.id, i])); const typeById = new Map(informationTypes.map((t) => [t.id, t])); - /** Which node runs an integration. A route or an attachment is drawn as its gateway. */ + /** Which node runs a subscription. A route or an attachment is drawn as its gateway. */ const runBy = new Map(); // ——— Gateways always appear, even with nothing attached: an idle gateway is a @@ -92,7 +92,7 @@ export function buildFlowGraph({ href: `/api-gateways/${g.id}`, ...(g.attachments.length === 0 ? { warning: "No partner is attached, so nothing can post to it." } : {}), }); - for (const a of g.attachments) runBy.set(a.integrationId, `apigw:${g.id}`); + for (const a of g.attachments) runBy.set(a.subscriptionId, `apigw:${g.id}`); } for (const g of busGateways) { @@ -109,7 +109,7 @@ export function buildFlowGraph({ href: `/bus-gateways/${g.id}`, ...(g.routes.length === 0 ? { warning: "It has no routes, so every message it receives is dropped." } : {}), }); - for (const r of g.routes) runBy.set(r.integrationId, `busgw:${g.id}`); + for (const r of g.routes) runBy.set(r.subscriptionId, `busgw:${g.id}`); } // ——— Every bus-enabled information type is a message node. Listed even when @@ -139,11 +139,11 @@ export function buildFlowGraph({ else warn(`busgw:${g.id}`, `Its information type is not on the bus, so no message can reach it.`); } - /** A node for an integration no gateway runs — reached only through a response. */ + /** A node for a subscription no gateway runs — reached only through a response. */ const standalone = (id: number): string | null => { - const i = integrationById.get(id); + const i = subscriptionById.get(id); if (!i) return null; - const kind: FlowNodeKind = JOB_TYPES.has(i.type) ? "job" : "integration"; + const kind: FlowNodeKind = JOB_TYPES.has(i.type) ? "job" : "subscription"; const nodeId = `${kind === "job" ? "job" : "int"}:${id}`; if (!nodes.has(nodeId)) nodes.set(nodeId, { @@ -157,8 +157,8 @@ export function buildFlowGraph({ return nodeId; }; - for (const i of integrations) { - if (i.responseMessageTypeName === null && i.responseIntegrationId === null) continue; + for (const i of subscriptions) { + if (i.responseMessageTypeName === null && i.responseSubscriptionId === null) continue; const from = runBy.get(i.id) ?? standalone(i.id); if (from === null) continue; @@ -166,7 +166,7 @@ export function buildFlowGraph({ // one to route: `XchangeService.RunHandler` returns null without a handler, and both // response paths are gated on that file being non-null. Drawing the edges anyway would // make the map assert a flow the runtime cannot produce — the exact kind of lie it - // exists to catch. The integration's own page hides these fields once the delivery is + // exists to catch. The subscription's own page hides these fields once the delivery is // cleared, which is what lets the pair drift apart unnoticed. if (i.handlerId === null) { warn(from, `${i.name} routes its response but delivers nothing, so no response is ever produced.`); @@ -181,8 +181,8 @@ export function buildFlowGraph({ else warn(from, `${i.name} publishes “${i.responseMessageTypeName}”, which no information type carries.`); } - if (i.responseIntegrationId !== null) { - const to = runBy.get(i.responseIntegrationId) ?? standalone(i.responseIntegrationId); + if (i.responseSubscriptionId !== null) { + const to = runBy.get(i.responseSubscriptionId) ?? standalone(i.responseSubscriptionId); // Same node on both ends means one of a gateway's routes hands off to another // of its own — real, but a loop drawn onto itself; its studio shows the chain. if (to !== null && to !== from) edges.push({ id: `hand:${i.id}`, from, to, kind: "handsOff" }); @@ -196,7 +196,7 @@ export function buildFlowGraph({ if (node.kind === "message" && !hasListener.has(node.id)) node.warning = "No gateway listens for this, so anything published under it is dropped."; - const omitted = integrations.filter( + const omitted = subscriptions.filter( (i) => !runBy.has(i.id) && !nodes.has(`int:${i.id}`) && !nodes.has(`job:${i.id}`), ).length; diff --git a/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetPage.tsx index 771e3663..1908b419 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetPage.tsx @@ -11,9 +11,9 @@ import { EditableTitle, Panel, UnsavedBar } from "../../components/ui/Panel"; import { MiniTable } from "../../components/ui/Table"; import { BackLink } from "../../components/ui/BackLink"; import { - INTEGRATION_TYPE_LABELS, - IntegrationMiniList, - useIntegrationsCache, + SUBSCRIPTION_TYPE_LABELS, + SubscriptionMiniList, + useSubscriptionsCache, } from "../../components/config/shared"; export function GlobalValueSetPage() { @@ -27,7 +27,7 @@ export function GlobalValueSetPage() { queryFn: () => api.getValueSet(id), retry: false, }); - const integrations = useIntegrationsCache(); + const subscriptions = useSubscriptionsCache(); const [name, setName] = useState(""); const [rows, setRows] = useState(null); @@ -107,13 +107,13 @@ export function GlobalValueSetPage() { emptyText="No values yet." rowDetails={(row) => { if (!row.key.trim()) return null; - const users = (integrations.data ?? []).filter((setup) => + const users = (subscriptions.data ?? []).filter((setup) => referencesGlobal(setup, id, row.key.trim()), ); return ( - ); }} @@ -124,28 +124,28 @@ export function GlobalValueSetPage() {
u.integrationSetup.id} + rowKey={(u) => u.subscriptionSetup.id} empty="Not referenced anywhere yet — safe to delete." columns={[ { - header: "Integration", + header: "Subscription", truncate: true, cell: (u) => ( - {u.integrationSetup.name} + {u.subscriptionSetup.name} ), }, { header: "Type", - cell: (u) => {INTEGRATION_TYPE_LABELS[u.integrationSetup.type]}, + cell: (u) => {SUBSCRIPTION_TYPE_LABELS[u.subscriptionSetup.type]}, }, { header: "References", @@ -180,7 +180,7 @@ export function GlobalValueSetPage() { body={ <> {s.name} and all its values will - be gone for good. Sets still referenced by integrations can't be deleted. + be gone for good. Sets still referenced by subscriptions can't be deleted. } confirmLabel="Delete value set" diff --git a/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetsPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetsPage.tsx index 5a632ca5..b770ea36 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetsPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/global-values/GlobalValueSetsPage.tsx @@ -9,7 +9,7 @@ import { Button, EmptyState, FormError, LoadingBlock } from "../../components/ui import { Field, TextInput } from "../../components/ui/forms"; import { Dialog } from "../../components/ui/overlays"; import { Table } from "../../components/ui/Table"; -import { UsedByCell, useIntegrationsCache } from "../../components/config/shared"; +import { UsedByCell, useSubscriptionsCache } from "../../components/config/shared"; import { suggestSlug } from "../../lib/identifiers"; function CreateValueSetDialog({ onClose }: { onClose: () => void }) { @@ -84,7 +84,7 @@ export function GlobalValueSetsPage() { const creating = searchParams.get("new") === "1"; const sets = useQuery({ queryKey: ["value-sets"], queryFn: () => api.listValueSets() }); - const integrations = useIntegrationsCache().data ?? []; + const subscriptions = useSubscriptionsCache().data ?? []; const setParam = (key: string, value: string | null) => setSearchParams( @@ -108,7 +108,7 @@ export function GlobalValueSetsPage() {