@shipfox/client-integrations 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +4 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +207 -0
  8. package/LICENSE +21 -0
  9. package/dist/components/connection-picker.d.ts +7 -0
  10. package/dist/components/connection-picker.d.ts.map +1 -0
  11. package/dist/components/connection-picker.js +98 -0
  12. package/dist/components/connection-picker.js.map +1 -0
  13. package/dist/components/installed-integrations-section.d.ts +14 -0
  14. package/dist/components/installed-integrations-section.d.ts.map +1 -0
  15. package/dist/components/installed-integrations-section.js +203 -0
  16. package/dist/components/installed-integrations-section.js.map +1 -0
  17. package/dist/components/integration-delete-confirm-modal.d.ts +10 -0
  18. package/dist/components/integration-delete-confirm-modal.d.ts.map +1 -0
  19. package/dist/components/integration-delete-confirm-modal.js +61 -0
  20. package/dist/components/integration-delete-confirm-modal.js.map +1 -0
  21. package/dist/components/integration-gallery-for-workspace.d.ts +9 -0
  22. package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -0
  23. package/dist/components/integration-gallery-for-workspace.js +169 -0
  24. package/dist/components/integration-gallery-for-workspace.js.map +1 -0
  25. package/dist/components/integration-gallery.d.ts +8 -0
  26. package/dist/components/integration-gallery.d.ts.map +1 -0
  27. package/dist/components/integration-gallery.js +26 -0
  28. package/dist/components/integration-gallery.js.map +1 -0
  29. package/dist/components/integration-gallery.stories.d.ts +26 -0
  30. package/dist/components/integration-gallery.stories.d.ts.map +1 -0
  31. package/dist/components/integration-gallery.stories.js +335 -0
  32. package/dist/components/integration-gallery.stories.js.map +1 -0
  33. package/dist/components/integration-usage-events.d.ts +4 -0
  34. package/dist/components/integration-usage-events.d.ts.map +1 -0
  35. package/dist/components/integration-usage-events.js +127 -0
  36. package/dist/components/integration-usage-events.js.map +1 -0
  37. package/dist/components/integration-usage-modal.d.ts +16 -0
  38. package/dist/components/integration-usage-modal.d.ts.map +1 -0
  39. package/dist/components/integration-usage-modal.js +184 -0
  40. package/dist/components/integration-usage-modal.js.map +1 -0
  41. package/dist/components/provider-grid.d.ts +14 -0
  42. package/dist/components/provider-grid.d.ts.map +1 -0
  43. package/dist/components/provider-grid.js +152 -0
  44. package/dist/components/provider-grid.js.map +1 -0
  45. package/dist/components/redirect-install-page.d.ts +19 -0
  46. package/dist/components/redirect-install-page.d.ts.map +1 -0
  47. package/dist/components/redirect-install-page.js +68 -0
  48. package/dist/components/redirect-install-page.js.map +1 -0
  49. package/dist/components/repository-picker.d.ts +15 -0
  50. package/dist/components/repository-picker.d.ts.map +1 -0
  51. package/dist/components/repository-picker.js +78 -0
  52. package/dist/components/repository-picker.js.map +1 -0
  53. package/dist/components/webhook/copyable-value.d.ts +10 -0
  54. package/dist/components/webhook/copyable-value.d.ts.map +1 -0
  55. package/dist/components/webhook/copyable-value.js +65 -0
  56. package/dist/components/webhook/copyable-value.js.map +1 -0
  57. package/dist/components/webhook/webhook-create-modal.d.ts +10 -0
  58. package/dist/components/webhook/webhook-create-modal.d.ts.map +1 -0
  59. package/dist/components/webhook/webhook-create-modal.js +233 -0
  60. package/dist/components/webhook/webhook-create-modal.js.map +1 -0
  61. package/dist/components/webhook/webhook-form-errors.d.ts +11 -0
  62. package/dist/components/webhook/webhook-form-errors.d.ts.map +1 -0
  63. package/dist/components/webhook/webhook-form-errors.js +27 -0
  64. package/dist/components/webhook/webhook-form-errors.js.map +1 -0
  65. package/dist/components/webhook/webhook-modals.stories.d.ts +25 -0
  66. package/dist/components/webhook/webhook-modals.stories.d.ts.map +1 -0
  67. package/dist/components/webhook/webhook-modals.stories.js +222 -0
  68. package/dist/components/webhook/webhook-modals.stories.js.map +1 -0
  69. package/dist/components/webhook/webhook-public-endpoint-alert.d.ts +2 -0
  70. package/dist/components/webhook/webhook-public-endpoint-alert.d.ts.map +1 -0
  71. package/dist/components/webhook/webhook-public-endpoint-alert.js +20 -0
  72. package/dist/components/webhook/webhook-public-endpoint-alert.js.map +1 -0
  73. package/dist/components/webhook/webhook-usage-details.d.ts +7 -0
  74. package/dist/components/webhook/webhook-usage-details.d.ts.map +1 -0
  75. package/dist/components/webhook/webhook-usage-details.js +49 -0
  76. package/dist/components/webhook/webhook-usage-details.js.map +1 -0
  77. package/dist/connection-status-badge.d.ts +12 -0
  78. package/dist/connection-status-badge.d.ts.map +1 -0
  79. package/dist/connection-status-badge.js +34 -0
  80. package/dist/connection-status-badge.js.map +1 -0
  81. package/dist/feature.d.ts +37 -0
  82. package/dist/feature.d.ts.map +1 -0
  83. package/dist/feature.js +48 -0
  84. package/dist/feature.js.map +1 -0
  85. package/dist/hooks/api/integrations.d.ts +265 -0
  86. package/dist/hooks/api/integrations.d.ts.map +1 -0
  87. package/dist/hooks/api/integrations.js +229 -0
  88. package/dist/hooks/api/integrations.js.map +1 -0
  89. package/dist/hooks/api/webhook-connections.d.ts +91 -0
  90. package/dist/hooks/api/webhook-connections.d.ts.map +1 -0
  91. package/dist/hooks/api/webhook-connections.js +97 -0
  92. package/dist/hooks/api/webhook-connections.js.map +1 -0
  93. package/dist/index.d.ts +17 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +18 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/integration-icon.d.ts +17 -0
  98. package/dist/integration-icon.d.ts.map +1 -0
  99. package/dist/integration-icon.js +22 -0
  100. package/dist/integration-icon.js.map +1 -0
  101. package/dist/linear-callback.d.ts +17 -0
  102. package/dist/linear-callback.d.ts.map +1 -0
  103. package/dist/linear-callback.js +113 -0
  104. package/dist/linear-callback.js.map +1 -0
  105. package/dist/pages/gitea-form-errors.d.ts +11 -0
  106. package/dist/pages/gitea-form-errors.d.ts.map +1 -0
  107. package/dist/pages/gitea-form-errors.js +35 -0
  108. package/dist/pages/gitea-form-errors.js.map +1 -0
  109. package/dist/pages/gitea-install-page.d.ts +2 -0
  110. package/dist/pages/gitea-install-page.d.ts.map +1 -0
  111. package/dist/pages/gitea-install-page.js +139 -0
  112. package/dist/pages/gitea-install-page.js.map +1 -0
  113. package/dist/pages/github-install-page.d.ts +2 -0
  114. package/dist/pages/github-install-page.d.ts.map +1 -0
  115. package/dist/pages/github-install-page.js +11 -0
  116. package/dist/pages/github-install-page.js.map +1 -0
  117. package/dist/pages/linear-callback-page.d.ts +2 -0
  118. package/dist/pages/linear-callback-page.d.ts.map +1 -0
  119. package/dist/pages/linear-callback-page.js +192 -0
  120. package/dist/pages/linear-callback-page.js.map +1 -0
  121. package/dist/pages/linear-install-page.d.ts +2 -0
  122. package/dist/pages/linear-install-page.d.ts.map +1 -0
  123. package/dist/pages/linear-install-page.js +24 -0
  124. package/dist/pages/linear-install-page.js.map +1 -0
  125. package/dist/pages/sentry-callback-page.d.ts +2 -0
  126. package/dist/pages/sentry-callback-page.d.ts.map +1 -0
  127. package/dist/pages/sentry-callback-page.js +256 -0
  128. package/dist/pages/sentry-callback-page.js.map +1 -0
  129. package/dist/pages/sentry-install-page.d.ts +2 -0
  130. package/dist/pages/sentry-install-page.d.ts.map +1 -0
  131. package/dist/pages/sentry-install-page.js +17 -0
  132. package/dist/pages/sentry-install-page.js.map +1 -0
  133. package/dist/pages/source-control-onboarding-page.d.ts +2 -0
  134. package/dist/pages/source-control-onboarding-page.d.ts.map +1 -0
  135. package/dist/pages/source-control-onboarding-page.js +37 -0
  136. package/dist/pages/source-control-onboarding-page.js.map +1 -0
  137. package/dist/provider-catalog.d.ts +15 -0
  138. package/dist/provider-catalog.d.ts.map +1 -0
  139. package/dist/provider-catalog.js +28 -0
  140. package/dist/provider-catalog.js.map +1 -0
  141. package/dist/routes/gitea.d.ts +6 -0
  142. package/dist/routes/gitea.d.ts.map +1 -0
  143. package/dist/routes/gitea.js +7 -0
  144. package/dist/routes/gitea.js.map +1 -0
  145. package/dist/routes/github-callback.d.ts +6 -0
  146. package/dist/routes/github-callback.d.ts.map +1 -0
  147. package/dist/routes/github-callback.js +129 -0
  148. package/dist/routes/github-callback.js.map +1 -0
  149. package/dist/routes/github.d.ts +6 -0
  150. package/dist/routes/github.d.ts.map +1 -0
  151. package/dist/routes/github.js +7 -0
  152. package/dist/routes/github.js.map +1 -0
  153. package/dist/routes/integrations.d.ts +6 -0
  154. package/dist/routes/integrations.d.ts.map +1 -0
  155. package/dist/routes/integrations.js +7 -0
  156. package/dist/routes/integrations.js.map +1 -0
  157. package/dist/routes/linear-callback.d.ts +6 -0
  158. package/dist/routes/linear-callback.d.ts.map +1 -0
  159. package/dist/routes/linear-callback.js +7 -0
  160. package/dist/routes/linear-callback.js.map +1 -0
  161. package/dist/routes/linear.d.ts +6 -0
  162. package/dist/routes/linear.d.ts.map +1 -0
  163. package/dist/routes/linear.js +7 -0
  164. package/dist/routes/linear.js.map +1 -0
  165. package/dist/routes/sentry-callback.d.ts +6 -0
  166. package/dist/routes/sentry-callback.d.ts.map +1 -0
  167. package/dist/routes/sentry-callback.js +7 -0
  168. package/dist/routes/sentry-callback.js.map +1 -0
  169. package/dist/routes/sentry.d.ts +6 -0
  170. package/dist/routes/sentry.d.ts.map +1 -0
  171. package/dist/routes/sentry.js +7 -0
  172. package/dist/routes/sentry.js.map +1 -0
  173. package/dist/sentry-callback.d.ts +35 -0
  174. package/dist/sentry-callback.d.ts.map +1 -0
  175. package/dist/sentry-callback.js +132 -0
  176. package/dist/sentry-callback.js.map +1 -0
  177. package/dist/tsconfig.test.tsbuildinfo +1 -0
  178. package/package.json +120 -0
  179. package/src/components/connection-picker.tsx +89 -0
  180. package/src/components/installed-integrations-section.tsx +198 -0
  181. package/src/components/integration-delete-confirm-modal.tsx +62 -0
  182. package/src/components/integration-gallery-for-workspace.tsx +193 -0
  183. package/src/components/integration-gallery.stories.tsx +302 -0
  184. package/src/components/integration-gallery.test.tsx +692 -0
  185. package/src/components/integration-gallery.tsx +49 -0
  186. package/src/components/integration-usage-events.test.ts +80 -0
  187. package/src/components/integration-usage-events.ts +116 -0
  188. package/src/components/integration-usage-modal.tsx +178 -0
  189. package/src/components/provider-grid.tsx +163 -0
  190. package/src/components/redirect-install-page.test.tsx +98 -0
  191. package/src/components/redirect-install-page.tsx +68 -0
  192. package/src/components/repository-picker.tsx +102 -0
  193. package/src/components/webhook/copyable-value.test.tsx +41 -0
  194. package/src/components/webhook/copyable-value.tsx +72 -0
  195. package/src/components/webhook/webhook-create-modal.tsx +247 -0
  196. package/src/components/webhook/webhook-form-errors.test.ts +45 -0
  197. package/src/components/webhook/webhook-form-errors.ts +32 -0
  198. package/src/components/webhook/webhook-modals.stories.tsx +225 -0
  199. package/src/components/webhook/webhook-public-endpoint-alert.tsx +15 -0
  200. package/src/components/webhook/webhook-usage-details.tsx +44 -0
  201. package/src/connection-status-badge.tsx +38 -0
  202. package/src/feature.ts +47 -0
  203. package/src/hooks/api/integrations.test.ts +105 -0
  204. package/src/hooks/api/integrations.ts +287 -0
  205. package/src/hooks/api/webhook-connections.ts +115 -0
  206. package/src/index.ts +16 -0
  207. package/src/integration-icon.react.test.tsx +40 -0
  208. package/src/integration-icon.test.ts +19 -0
  209. package/src/integration-icon.tsx +26 -0
  210. package/src/linear-callback.test.ts +227 -0
  211. package/src/linear-callback.ts +136 -0
  212. package/src/pages/gitea-form-errors.test.ts +55 -0
  213. package/src/pages/gitea-form-errors.ts +36 -0
  214. package/src/pages/gitea-install-page.tsx +108 -0
  215. package/src/pages/github-install-page.tsx +11 -0
  216. package/src/pages/linear-callback-page.test.tsx +157 -0
  217. package/src/pages/linear-callback-page.tsx +175 -0
  218. package/src/pages/linear-install-page.tsx +26 -0
  219. package/src/pages/sentry-callback-page.test.tsx +205 -0
  220. package/src/pages/sentry-callback-page.tsx +238 -0
  221. package/src/pages/sentry-install-page.tsx +17 -0
  222. package/src/pages/source-control-onboarding-page.test.tsx +46 -0
  223. package/src/pages/source-control-onboarding-page.tsx +26 -0
  224. package/src/provider-catalog.test.ts +42 -0
  225. package/src/provider-catalog.ts +49 -0
  226. package/src/routes/gitea.tsx +4 -0
  227. package/src/routes/github-callback.tsx +114 -0
  228. package/src/routes/github.tsx +4 -0
  229. package/src/routes/integrations.tsx +4 -0
  230. package/src/routes/linear-callback.tsx +4 -0
  231. package/src/routes/linear.tsx +4 -0
  232. package/src/routes/sentry-callback.tsx +4 -0
  233. package/src/routes/sentry.tsx +4 -0
  234. package/src/sentry-callback.test.ts +239 -0
  235. package/src/sentry-callback.ts +142 -0
  236. package/test/render.tsx +111 -0
  237. package/test/setup.ts +3 -0
  238. package/tsconfig.build.json +9 -0
  239. package/tsconfig.build.tsbuildinfo +1 -0
  240. package/tsconfig.json +3 -0
  241. package/tsconfig.test.json +8 -0
  242. package/vercel.json +8 -0
  243. package/vitest.config.ts +75 -0
@@ -0,0 +1,302 @@
1
+ import type {
2
+ IntegrationConnectionDto,
3
+ IntegrationProviderDto,
4
+ } from '@shipfox/api-integration-core-dto';
5
+ import {configureApiClient} from '@shipfox/client-api';
6
+ import {authStateAtom} from '@shipfox/client-auth';
7
+ import {Toaster} from '@shipfox/react-ui/toast';
8
+ import type {Meta, StoryObj} from '@storybook/react';
9
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
10
+ import {
11
+ createMemoryHistory,
12
+ createRootRoute,
13
+ createRoute,
14
+ createRouter,
15
+ Outlet,
16
+ RouterProvider,
17
+ } from '@tanstack/react-router';
18
+ import {createStore, Provider as JotaiProvider} from 'jotai';
19
+ import {useMemo} from 'react';
20
+ import {IntegrationGallery} from './integration-gallery.js';
21
+
22
+ const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
23
+ const WORKSPACE_PATH = `/workspaces/${WORKSPACE_ID}/settings/integrations`;
24
+ const SETUP_PATHS = [
25
+ '/workspaces/$wid/integrations/github',
26
+ '/workspaces/$wid/integrations/sentry',
27
+ '/workspaces/$wid/integrations/linear',
28
+ '/workspaces/$wid/integrations/gitea',
29
+ '/workspaces/$wid/settings/events',
30
+ ] as const;
31
+
32
+ type Scenario =
33
+ | 'mixed'
34
+ | 'empty-connections'
35
+ | 'loading'
36
+ | 'connections-error'
37
+ | 'providers-error'
38
+ | 'no-providers'
39
+ | 'long-names';
40
+
41
+ interface IntegrationGalleryStoryProps {
42
+ scenario: Scenario;
43
+ }
44
+
45
+ const PROVIDERS: IntegrationProviderDto[] = [
46
+ {provider: 'github', display_name: 'GitHub', capabilities: ['source_control']},
47
+ {provider: 'sentry', display_name: 'Sentry', capabilities: []},
48
+ {provider: 'linear', display_name: 'Linear', capabilities: ['agent_tools']},
49
+ {provider: 'gitea', display_name: 'Gitea', capabilities: ['source_control']},
50
+ {provider: 'webhook', display_name: 'Webhook', capabilities: []},
51
+ ];
52
+
53
+ function IntegrationGalleryStory({scenario}: IntegrationGalleryStoryProps) {
54
+ configureApiClient({
55
+ baseUrl: 'https://api.example.test',
56
+ fetchImpl: fetchForScenario(scenario),
57
+ });
58
+
59
+ const queryClient = useMemo(
60
+ () => new QueryClient({defaultOptions: {queries: {retry: false}}}),
61
+ [],
62
+ );
63
+ const store = useMemo(() => {
64
+ const nextStore = createStore();
65
+ nextStore.set(authStateAtom, {
66
+ status: 'authenticated',
67
+ workspaces: [{id: WORKSPACE_ID, name: 'Acme', membershipId: 'membership-1'}],
68
+ });
69
+ return nextStore;
70
+ }, []);
71
+ const router = useMemo(() => {
72
+ const rootRoute = createRootRoute({component: Outlet});
73
+ const workspaceRoute = createRoute({
74
+ getParentRoute: () => rootRoute,
75
+ path: '/workspaces/$wid',
76
+ component: Outlet,
77
+ });
78
+ const galleryRoute = createRoute({
79
+ getParentRoute: () => workspaceRoute,
80
+ path: 'settings/integrations',
81
+ component: () => (
82
+ <div className="mx-auto w-full max-w-[760px] bg-background-neutral-background p-24">
83
+ <IntegrationGallery workspaceId={WORKSPACE_ID} />
84
+ </div>
85
+ ),
86
+ });
87
+ const setupRoutes = SETUP_PATHS.map((path) =>
88
+ createRoute({
89
+ getParentRoute: () => workspaceRoute,
90
+ path: path.replace('/workspaces/$wid/', ''),
91
+ component: () => <div />,
92
+ }),
93
+ );
94
+
95
+ return createRouter({
96
+ history: createMemoryHistory({initialEntries: [WORKSPACE_PATH]}),
97
+ routeTree: rootRoute.addChildren([
98
+ workspaceRoute.addChildren([galleryRoute, ...setupRoutes]),
99
+ ]),
100
+ });
101
+ }, []);
102
+
103
+ return (
104
+ <QueryClientProvider client={queryClient}>
105
+ <JotaiProvider store={store}>
106
+ <RouterProvider router={router} />
107
+ <Toaster />
108
+ </JotaiProvider>
109
+ </QueryClientProvider>
110
+ );
111
+ }
112
+
113
+ const meta = {
114
+ title: 'Integrations/Gallery',
115
+ component: IntegrationGalleryStory,
116
+ parameters: {layout: 'fullscreen'},
117
+ args: {scenario: 'mixed'},
118
+ } satisfies Meta<typeof IntegrationGalleryStory>;
119
+
120
+ export default meta;
121
+ type Story = StoryObj<typeof meta>;
122
+
123
+ export const Playground: Story = {};
124
+
125
+ export const EmptyConnections: Story = {
126
+ args: {scenario: 'empty-connections'},
127
+ };
128
+
129
+ export const Loading: Story = {
130
+ args: {scenario: 'loading'},
131
+ };
132
+
133
+ export const ConnectionsError: Story = {
134
+ args: {scenario: 'connections-error'},
135
+ };
136
+
137
+ export const ProvidersError: Story = {
138
+ args: {scenario: 'providers-error'},
139
+ };
140
+
141
+ export const NoProvidersAvailable: Story = {
142
+ args: {scenario: 'no-providers'},
143
+ };
144
+
145
+ export const LongNames: Story = {
146
+ args: {scenario: 'long-names'},
147
+ };
148
+
149
+ function fetchForScenario(scenario: Scenario): typeof fetch {
150
+ return (input, init) => {
151
+ const url = requestUrl(input);
152
+ const method = init?.method ?? 'GET';
153
+ if (scenario === 'loading') return new Promise<Response>(() => undefined);
154
+ if (url.pathname === '/integration-providers') {
155
+ if (scenario === 'providers-error') return Promise.resolve(errorResponse());
156
+ return Promise.resolve(
157
+ jsonResponse({providers: scenario === 'no-providers' ? [] : PROVIDERS}),
158
+ );
159
+ }
160
+ if (url.pathname.startsWith('/integration-connections/') && method === 'PATCH') {
161
+ return Promise.resolve(
162
+ jsonResponse({
163
+ ...connection(),
164
+ id: url.pathname.split('/').at(-1),
165
+ lifecycle_status: 'disabled',
166
+ }),
167
+ );
168
+ }
169
+ if (url.pathname.startsWith('/integration-connections/') && method === 'DELETE') {
170
+ return Promise.resolve(jsonResponse(undefined, {status: 204}));
171
+ }
172
+ if (url.pathname.startsWith('/integrations/webhook/connections/') && method === 'PATCH') {
173
+ return Promise.resolve(
174
+ jsonResponse({
175
+ id: url.pathname.split('/').at(-1),
176
+ workspace_id: WORKSPACE_ID,
177
+ name: 'Stripe production',
178
+ slug: 'stripe-prod',
179
+ lifecycle_status: 'disabled',
180
+ inbound_url: 'https://api.example.test/webhook/77777777-7777-4777-8777-777777777777',
181
+ created_at: '2026-04-12T00:00:00.000Z',
182
+ updated_at: '2026-04-12T00:00:00.000Z',
183
+ }),
184
+ );
185
+ }
186
+ if (url.pathname.startsWith('/integrations/webhook/connections/') && method === 'DELETE') {
187
+ return Promise.resolve(jsonResponse(undefined, {status: 204}));
188
+ }
189
+ if (url.pathname === '/integration-connections') {
190
+ if (scenario === 'connections-error') return Promise.resolve(errorResponse());
191
+ return Promise.resolve(jsonResponse({connections: connectionsForScenario(scenario)}));
192
+ }
193
+ if (url.pathname === '/integrations/webhook/connections') {
194
+ return Promise.resolve(
195
+ jsonResponse({
196
+ connections: [
197
+ {
198
+ id: '77777777-7777-4777-8777-777777777777',
199
+ workspace_id: WORKSPACE_ID,
200
+ name: 'Stripe production',
201
+ slug: 'stripe-prod',
202
+ lifecycle_status: 'active',
203
+ inbound_url: 'https://api.example.test/webhook/77777777-7777-4777-8777-777777777777',
204
+ created_at: '2026-04-12T00:00:00.000Z',
205
+ updated_at: '2026-04-12T00:00:00.000Z',
206
+ },
207
+ ],
208
+ }),
209
+ );
210
+ }
211
+ return Promise.resolve(jsonResponse({}, {status: 404}));
212
+ };
213
+ }
214
+
215
+ function connectionsForScenario(scenario: Scenario): IntegrationConnectionDto[] {
216
+ if (scenario === 'empty-connections' || scenario === 'no-providers') return [];
217
+ if (scenario === 'long-names') {
218
+ return [
219
+ connection({
220
+ display_name: 'acme-production-observability-and-source-control-organization',
221
+ lifecycle_status: 'active',
222
+ }),
223
+ connection({
224
+ id: '55555555-5555-4555-8555-555555555555',
225
+ provider: 'sentry',
226
+ display_name: 'sentry-team-with-a-very-long-connected-account-name',
227
+ lifecycle_status: 'error',
228
+ }),
229
+ ];
230
+ }
231
+ return [
232
+ connection({display_name: 'acme-corp', lifecycle_status: 'active'}),
233
+ connection({
234
+ id: '88888888-8888-4888-8888-888888888888',
235
+ provider: 'linear',
236
+ display_name: 'linear-acme',
237
+ lifecycle_status: 'active',
238
+ external_url: 'https://linear.app/acme/settings',
239
+ }),
240
+ connection({
241
+ id: '55555555-5555-4555-8555-555555555555',
242
+ provider: 'linear',
243
+ display_name: 'linear-disabled',
244
+ lifecycle_status: 'disabled',
245
+ external_url: undefined,
246
+ }),
247
+ connection({
248
+ id: '66666666-6666-4666-8666-666666666666',
249
+ provider: 'linear',
250
+ display_name: 'linear-error',
251
+ lifecycle_status: 'error',
252
+ external_url: undefined,
253
+ }),
254
+ connection({
255
+ id: '77777777-7777-4777-8777-777777777777',
256
+ provider: 'webhook',
257
+ external_account_id: 'stripe-prod',
258
+ slug: 'stripe-prod',
259
+ display_name: 'Stripe production',
260
+ capabilities: [],
261
+ lifecycle_status: 'active',
262
+ external_url: undefined,
263
+ created_at: '2026-04-12T00:00:00.000Z',
264
+ updated_at: '2026-04-12T00:00:00.000Z',
265
+ }),
266
+ ];
267
+ }
268
+
269
+ function connection(overrides: Partial<IntegrationConnectionDto> = {}): IntegrationConnectionDto {
270
+ return {
271
+ id: '44444444-4444-4444-8444-444444444444',
272
+ workspace_id: WORKSPACE_ID,
273
+ provider: 'github',
274
+ external_account_id: 'installation-1',
275
+ slug: 'github_acme_corp',
276
+ display_name: 'acme-corp',
277
+ lifecycle_status: 'active',
278
+ capabilities: ['source_control'],
279
+ external_url: 'https://github.com/organizations/acme-corp/settings/installations/1',
280
+ created_at: '2026-03-12T00:00:00.000Z',
281
+ updated_at: '2026-03-12T00:00:00.000Z',
282
+ ...overrides,
283
+ };
284
+ }
285
+
286
+ function requestUrl(input: RequestInfo | URL): URL {
287
+ if (typeof input === 'string') return new URL(input);
288
+ if (input instanceof URL) return input;
289
+ return new URL(input.url);
290
+ }
291
+
292
+ function jsonResponse(body: unknown, init: ResponseInit = {}) {
293
+ return new Response(JSON.stringify(body), {
294
+ status: 200,
295
+ headers: {'content-type': 'application/json'},
296
+ ...init,
297
+ });
298
+ }
299
+
300
+ function errorResponse() {
301
+ return jsonResponse({code: 'server-error'}, {status: 500, statusText: 'Server error'});
302
+ }