@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
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@shipfox/client-integrations",
3
+ "license": "MIT",
4
+ "version": "0.2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
8
+ "directory": "libs/client/integrations"
9
+ },
10
+ "type": "module",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "imports": {
14
+ "#*": {
15
+ "types": "./src/*",
16
+ "workspace-source": "./src/*",
17
+ "development": "./src/*",
18
+ "default": "./dist/*"
19
+ },
20
+ "#test/*": "./test/*"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "development": {
25
+ "types": "./src/index.ts",
26
+ "default": "./src/index.ts"
27
+ },
28
+ "default": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "./feature": {
34
+ "development": {
35
+ "types": "./src/feature.ts",
36
+ "default": "./src/feature.ts"
37
+ },
38
+ "default": {
39
+ "types": "./dist/feature.d.ts",
40
+ "default": "./dist/feature.js"
41
+ }
42
+ },
43
+ "./routes/*": {
44
+ "development": {
45
+ "types": "./src/routes/*.tsx",
46
+ "default": "./src/routes/*.tsx"
47
+ },
48
+ "default": {
49
+ "types": "./dist/routes/*.d.ts",
50
+ "default": "./dist/routes/*.js"
51
+ }
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "@swc/helpers": "^0.5.17",
56
+ "@tanstack/react-form": "^1.32.0",
57
+ "@shipfox/api-integration-core-dto": "3.0.0",
58
+ "@shipfox/api-integration-gitea-dto": "3.0.0",
59
+ "@shipfox/api-integration-github-dto": "3.0.0",
60
+ "@shipfox/api-integration-sentry-dto": "3.0.0",
61
+ "@shipfox/api-integration-linear-dto": "3.0.0",
62
+ "@shipfox/client-api": "0.2.0",
63
+ "@shipfox/api-integration-webhook-dto": "3.0.0",
64
+ "@shipfox/client-auth": "0.2.0",
65
+ "@shipfox/client-shell": "0.2.0",
66
+ "@shipfox/react-ui": "0.3.1",
67
+ "@shipfox/client-ui": "0.2.0"
68
+ },
69
+ "peerDependencies": {
70
+ "@tanstack/react-query": "^5.101.0",
71
+ "@tanstack/react-router": "^1.170.16",
72
+ "jotai": "^2.19.1",
73
+ "react": "^19.0.0",
74
+ "react-dom": "^19.0.0"
75
+ },
76
+ "devDependencies": {
77
+ "@argos-ci/cli": "^5.0.0",
78
+ "@argos-ci/storybook": "^6.0.0",
79
+ "@storybook/addon-vitest": "^10.3.6",
80
+ "@storybook/react": "^10.0.0",
81
+ "@storybook/react-vite": "^10.0.0",
82
+ "@tailwindcss/vite": "^4.1.13",
83
+ "@tanstack/react-query": "^5.101.0",
84
+ "@tanstack/react-router": "^1.170.16",
85
+ "@testing-library/jest-dom": "^6.6.3",
86
+ "@testing-library/react": "^16.3.2",
87
+ "@testing-library/user-event": "^14.6.1",
88
+ "@types/react": "^19.1.11",
89
+ "@types/react-dom": "^19.1.7",
90
+ "@vitejs/plugin-react": "^6.0.0",
91
+ "@vitest/browser-playwright": "^4.1.5",
92
+ "jotai": "^2.19.1",
93
+ "jsdom": "^29.0.0",
94
+ "react": "^19.1.1",
95
+ "react-dom": "^19.1.1",
96
+ "storybook": "^10.0.0",
97
+ "storybook-addon-pseudo-states": "^10.0.0",
98
+ "tailwindcss": "^4.1.13",
99
+ "vite": "^8.0.3",
100
+ "vitest": "^4.1.5",
101
+ "@shipfox/biome": "1.8.1",
102
+ "@shipfox/swc": "1.2.5",
103
+ "@shipfox/client-test-setup": "0.0.2",
104
+ "@shipfox/typescript": "1.1.6",
105
+ "@shipfox/ts-config": "1.3.8",
106
+ "@shipfox/vite": "1.2.4",
107
+ "@shipfox/vitest": "1.2.2"
108
+ },
109
+ "scripts": {
110
+ "build": "shipfox-swc",
111
+ "check": "shipfox-biome-check",
112
+ "check:fix": "shipfox-biome-check --write",
113
+ "storybook": "storybook dev -p 6011",
114
+ "storybook:build": "storybook build -o storybook-static",
115
+ "test": "shipfox-vitest-run",
116
+ "test:watch": "shipfox-vitest-watch",
117
+ "type": "shipfox-tsc-check",
118
+ "type:emit": "shipfox-tsc-emit"
119
+ }
120
+ }
@@ -0,0 +1,89 @@
1
+ import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
2
+ import {useIsTextTruncated} from '@shipfox/react-ui/hooks';
3
+ import {Label} from '@shipfox/react-ui/label';
4
+ import {RadioGroup, RadioGroupItem} from '@shipfox/react-ui/radio-group';
5
+ import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
6
+ import {Text} from '@shipfox/react-ui/typography';
7
+ import {useId} from 'react';
8
+ import {IntegrationIcon} from '#integration-icon.js';
9
+
10
+ export function ConnectionPicker({
11
+ connections,
12
+ selectedConnectionId,
13
+ onSelect,
14
+ }: {
15
+ connections: IntegrationConnectionDto[];
16
+ selectedConnectionId: string | undefined;
17
+ onSelect: (connectionId: string) => void;
18
+ }) {
19
+ const labelId = useId();
20
+
21
+ return (
22
+ <div className="flex flex-col gap-10">
23
+ <Label id={labelId} className="sr-only">
24
+ Source integration
25
+ </Label>
26
+ <RadioGroup
27
+ aria-labelledby={labelId}
28
+ value={selectedConnectionId ?? ''}
29
+ onValueChange={onSelect}
30
+ className="grid grid-cols-2 gap-8 min-[1200px]:grid-cols-3 max-[760px]:grid-cols-1"
31
+ >
32
+ {connections.map((connection) => (
33
+ <RadioGroupItem key={connection.id} value={connection.id} className="p-12">
34
+ <ConnectionOption connection={connection} />
35
+ </RadioGroupItem>
36
+ ))}
37
+ </RadioGroup>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ function ConnectionOption({connection}: {connection: IntegrationConnectionDto}) {
43
+ const {ref: nameRef, isTruncated} = useIsTextTruncated<HTMLSpanElement>(connection.display_name);
44
+
45
+ return (
46
+ <span className="flex min-w-0 items-center gap-10">
47
+ <Tooltip>
48
+ <TooltipTrigger asChild>
49
+ <span className="shrink-0">
50
+ <IntegrationIcon
51
+ source={connection.provider}
52
+ aria-hidden
53
+ className="size-20 text-foreground-neutral-base"
54
+ />
55
+ </span>
56
+ </TooltipTrigger>
57
+ <TooltipContent>{integrationName(connection.provider)}</TooltipContent>
58
+ </Tooltip>
59
+
60
+ <Tooltip>
61
+ <TooltipTrigger asChild>
62
+ <span ref={nameRef} className="min-w-0 truncate">
63
+ <Text as="span" size="sm" bold>
64
+ {connection.display_name}
65
+ </Text>
66
+ </span>
67
+ </TooltipTrigger>
68
+ {isTruncated ? <TooltipContent>{connection.display_name}</TooltipContent> : null}
69
+ </Tooltip>
70
+ </span>
71
+ );
72
+ }
73
+
74
+ function integrationName(provider: string): string {
75
+ switch (provider) {
76
+ case 'github':
77
+ return 'GitHub';
78
+ case 'gitea':
79
+ return 'Gitea';
80
+ case 'sentry':
81
+ return 'Sentry';
82
+ case 'webhook':
83
+ return 'Webhook';
84
+ case 'debug':
85
+ return 'Debug';
86
+ default:
87
+ return provider;
88
+ }
89
+ }
@@ -0,0 +1,198 @@
1
+ import type {
2
+ IntegrationConnectionDto,
3
+ ListIntegrationConnectionsResponseDto,
4
+ } from '@shipfox/api-integration-core-dto';
5
+ import {QueryLoadError} from '@shipfox/client-ui';
6
+ import {IconButton} from '@shipfox/react-ui/button';
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from '@shipfox/react-ui/dropdown-menu';
14
+ import {EmptyState} from '@shipfox/react-ui/empty-state';
15
+ import {Skeleton} from '@shipfox/react-ui/skeleton';
16
+ import {Header, Text} from '@shipfox/react-ui/typography';
17
+ import {cn, formatDate} from '@shipfox/react-ui/utils';
18
+ import type {UseQueryResult} from '@tanstack/react-query';
19
+ import {Link} from '@tanstack/react-router';
20
+ import {ConnectionStatusBadge} from '#connection-status-badge.js';
21
+ import {IntegrationIcon} from '#integration-icon.js';
22
+ import {usageEventsForConnection} from './integration-usage-events.js';
23
+
24
+ interface InstalledIntegrationsSectionProps {
25
+ connectionsQuery: UseQueryResult<ListIntegrationConnectionsResponseDto, Error>;
26
+ connections: IntegrationConnectionDto[];
27
+ isMutating: boolean;
28
+ onUse: (connectionId: string) => void;
29
+ onSetActive: (connection: IntegrationConnectionDto, active: boolean) => void;
30
+ onDelete: (connectionId: string) => void;
31
+ providerDisplayName: (provider: string) => string | undefined;
32
+ }
33
+
34
+ const INSTALLED_SURFACE_CLASS =
35
+ 'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
36
+
37
+ export function InstalledIntegrationsSection({
38
+ connectionsQuery,
39
+ connections,
40
+ isMutating,
41
+ onUse,
42
+ onSetActive,
43
+ onDelete,
44
+ providerDisplayName,
45
+ }: InstalledIntegrationsSectionProps) {
46
+ return (
47
+ <section className="flex flex-col gap-16" aria-label="Installed integrations">
48
+ <div className="flex flex-col gap-4">
49
+ <Header variant="h3">Installed integrations</Header>
50
+ <Text size="sm" className="text-foreground-neutral-muted">
51
+ Provider accounts installed in this workspace.
52
+ </Text>
53
+ </div>
54
+
55
+ {connectionsQuery.isPending ? <InstalledSkeleton label="Loading integrations" /> : null}
56
+
57
+ {connectionsQuery.isError && connectionsQuery.data === undefined ? (
58
+ <div className={cn(INSTALLED_SURFACE_CLASS, 'px-16')}>
59
+ <QueryLoadError query={connectionsQuery} subject="integrations" />
60
+ </div>
61
+ ) : null}
62
+
63
+ {connectionsQuery.data !== undefined && connections.length === 0 ? (
64
+ <div className={cn(INSTALLED_SURFACE_CLASS, 'px-16')}>
65
+ <EmptyState
66
+ icon="componentLine"
67
+ title="No integrations installed yet"
68
+ description="Install a provider below to get started."
69
+ />
70
+ </div>
71
+ ) : null}
72
+
73
+ {connections.length > 0 ? (
74
+ <ul className={cn('divide-y divide-border-neutral-base', INSTALLED_SURFACE_CLASS)}>
75
+ {connections.map((connection) => (
76
+ <InstalledRow
77
+ key={connection.id}
78
+ connection={connection}
79
+ isMutating={isMutating}
80
+ onUse={onUse}
81
+ onSetActive={(nextActive) => onSetActive(connection, nextActive)}
82
+ onDelete={onDelete}
83
+ providerDisplayName={providerDisplayName}
84
+ />
85
+ ))}
86
+ </ul>
87
+ ) : null}
88
+ </section>
89
+ );
90
+ }
91
+
92
+ function InstalledRow({
93
+ connection,
94
+ isMutating,
95
+ onUse,
96
+ onSetActive,
97
+ onDelete,
98
+ providerDisplayName,
99
+ }: {
100
+ connection: IntegrationConnectionDto;
101
+ isMutating: boolean;
102
+ onUse: (connectionId: string) => void;
103
+ onSetActive: (active: boolean) => void;
104
+ onDelete: (connectionId: string) => void;
105
+ providerDisplayName: (provider: string) => string | undefined;
106
+ }) {
107
+ const muted = connection.lifecycle_status === 'disabled';
108
+ const active = connection.lifecycle_status === 'active';
109
+ const recentEventsEvent = usageEventsForConnection(connection)[0]?.value ?? 'received';
110
+ const providerName = providerDisplayName(connection.provider);
111
+
112
+ return (
113
+ <li className="flex items-center gap-12 px-16 py-12 transition-colors hover:bg-background-components-hover">
114
+ <IntegrationIcon
115
+ source={connection.provider}
116
+ aria-hidden
117
+ className={cn(
118
+ 'size-24 shrink-0',
119
+ muted ? 'text-foreground-neutral-disabled' : 'text-foreground-neutral-base',
120
+ )}
121
+ />
122
+ <div className="flex min-w-0 flex-1 flex-col gap-2">
123
+ <div className="flex min-w-0 items-center gap-8">
124
+ <Text
125
+ size="md"
126
+ bold
127
+ className={cn('truncate', muted ? 'text-foreground-neutral-disabled' : undefined)}
128
+ >
129
+ {connection.display_name}
130
+ </Text>
131
+ <ConnectionStatusBadge status={connection.lifecycle_status} className="shrink-0" />
132
+ </div>
133
+ <Text size="sm" className="truncate text-foreground-neutral-muted">
134
+ Added {formatDate(connection.created_at)}
135
+ </Text>
136
+ </div>
137
+ <DropdownMenu>
138
+ <DropdownMenuTrigger asChild>
139
+ <IconButton
140
+ size="sm"
141
+ variant="transparent"
142
+ icon="more2Line"
143
+ aria-label={`Open ${connection.display_name} integration actions`}
144
+ />
145
+ </DropdownMenuTrigger>
146
+ <DropdownMenuContent align="end">
147
+ {connection.external_url ? (
148
+ <DropdownMenuItem asChild>
149
+ <a href={connection.external_url} target="_blank" rel="noreferrer noopener">
150
+ {providerName ? `Open in ${providerName}` : 'Open provider settings'}
151
+ </a>
152
+ </DropdownMenuItem>
153
+ ) : null}
154
+ <DropdownMenuItem onSelect={() => onUse(connection.id)}>
155
+ Use this integration
156
+ </DropdownMenuItem>
157
+ <DropdownMenuItem asChild>
158
+ <Link
159
+ to="/workspaces/$wid/settings/events"
160
+ params={{wid: connection.workspace_id}}
161
+ search={{source: [connection.slug], event: [recentEventsEvent]}}
162
+ >
163
+ View recent events
164
+ </Link>
165
+ </DropdownMenuItem>
166
+ <DropdownMenuSeparator />
167
+ <DropdownMenuItem disabled={isMutating} onSelect={() => onSetActive(!active)}>
168
+ {active ? 'Disable integration' : 'Enable integration'}
169
+ </DropdownMenuItem>
170
+ <DropdownMenuItem disabled={isMutating} onSelect={() => onDelete(connection.id)}>
171
+ Delete integration
172
+ </DropdownMenuItem>
173
+ </DropdownMenuContent>
174
+ </DropdownMenu>
175
+ </li>
176
+ );
177
+ }
178
+
179
+ function InstalledSkeleton({label}: {label: string}) {
180
+ return (
181
+ <ul
182
+ role="status"
183
+ aria-label={label}
184
+ className={cn('divide-y divide-border-neutral-base', INSTALLED_SURFACE_CLASS)}
185
+ >
186
+ {[0, 1, 2].map((row) => (
187
+ <li key={row} className="flex items-center gap-12 px-16 py-12">
188
+ <Skeleton className="size-24 shrink-0" />
189
+ <div className="flex min-w-0 flex-1 flex-col gap-2">
190
+ <Skeleton className="h-16 w-120" />
191
+ <Skeleton className="h-12 w-80" />
192
+ </div>
193
+ <Skeleton className="h-20 w-72 shrink-0" />
194
+ </li>
195
+ ))}
196
+ </ul>
197
+ );
198
+ }
@@ -0,0 +1,62 @@
1
+ import {Button} from '@shipfox/react-ui/button';
2
+ import {
3
+ Modal,
4
+ ModalBody,
5
+ ModalContent,
6
+ ModalFooter,
7
+ ModalHeader,
8
+ ModalTitle,
9
+ } from '@shipfox/react-ui/modal';
10
+ import {Text} from '@shipfox/react-ui/typography';
11
+
12
+ interface IntegrationDeleteConfirmModalProps {
13
+ connectionName: string | undefined;
14
+ open: boolean;
15
+ isPending: boolean;
16
+ onOpenChange: (open: boolean) => void;
17
+ onConfirm: () => void;
18
+ }
19
+
20
+ export function IntegrationDeleteConfirmModal({
21
+ connectionName,
22
+ open,
23
+ isPending,
24
+ onOpenChange,
25
+ onConfirm,
26
+ }: IntegrationDeleteConfirmModalProps) {
27
+ const name = connectionName ?? 'this integration';
28
+ return (
29
+ <Modal
30
+ open={open}
31
+ onOpenChange={(nextOpen) => {
32
+ if (isPending && !nextOpen) return;
33
+ onOpenChange(nextOpen);
34
+ }}
35
+ >
36
+ <ModalContent aria-describedby={undefined}>
37
+ <ModalTitle className="sr-only">Delete integration</ModalTitle>
38
+ <ModalHeader title="Delete integration" showClose={!isPending} />
39
+ <ModalBody className="gap-16">
40
+ <Text size="sm">
41
+ Are you sure you want to delete <strong className="font-medium">{name}</strong>? Once
42
+ deleted, Shipfox will immediately stop processing events from this integration. This
43
+ cannot be undone.
44
+ </Text>
45
+ </ModalBody>
46
+ <ModalFooter>
47
+ <Button
48
+ type="button"
49
+ variant="secondary"
50
+ disabled={isPending}
51
+ onClick={() => onOpenChange(false)}
52
+ >
53
+ Cancel
54
+ </Button>
55
+ <Button type="button" variant="danger" isLoading={isPending} onClick={onConfirm}>
56
+ Delete integration
57
+ </Button>
58
+ </ModalFooter>
59
+ </ModalContent>
60
+ </Modal>
61
+ );
62
+ }
@@ -0,0 +1,193 @@
1
+ import type {
2
+ IntegrationCapabilityDto,
3
+ IntegrationConnectionDto,
4
+ IntegrationProviderDto,
5
+ } from '@shipfox/api-integration-core-dto';
6
+ import {toast} from '@shipfox/react-ui/toast';
7
+ import {Header, Text} from '@shipfox/react-ui/typography';
8
+ import {useState} from 'react';
9
+ import {
10
+ useDeleteIntegrationConnectionMutation,
11
+ useIntegrationConnectionsQuery,
12
+ useIntegrationProvidersQuery,
13
+ useUpdateIntegrationConnectionMutation,
14
+ } from '#hooks/api/integrations.js';
15
+ import {
16
+ useDeleteWebhookConnectionMutation,
17
+ useUpdateWebhookConnectionMutation,
18
+ } from '#hooks/api/webhook-connections.js';
19
+ import {InstalledIntegrationsSection} from './installed-integrations-section.js';
20
+ import {IntegrationDeleteConfirmModal} from './integration-delete-confirm-modal.js';
21
+ import {usageEventsForConnection} from './integration-usage-events.js';
22
+ import {IntegrationUsageModal} from './integration-usage-modal.js';
23
+ import {ProviderGrid} from './provider-grid.js';
24
+ import {WebhookCreateModal} from './webhook/webhook-create-modal.js';
25
+ import {WebhookUsageDetails} from './webhook/webhook-usage-details.js';
26
+
27
+ interface IntegrationGalleryForWorkspaceProps {
28
+ capability: IntegrationCapabilityDto | undefined;
29
+ emptyProvidersMessage: string;
30
+ workspaceId: string;
31
+ }
32
+
33
+ export function IntegrationGalleryForWorkspace({
34
+ capability,
35
+ emptyProvidersMessage,
36
+ workspaceId,
37
+ }: IntegrationGalleryForWorkspaceProps) {
38
+ const [createProvider, setCreateProvider] = useState<string | undefined>();
39
+ const [usageConnectionId, setUsageConnectionId] = useState<string | undefined>();
40
+ const [createdUsageConnection, setCreatedUsageConnection] = useState<
41
+ IntegrationConnectionDto | undefined
42
+ >();
43
+ const [deleteConnectionId, setDeleteConnectionId] = useState<string | undefined>();
44
+ const providersQuery = useIntegrationProvidersQuery(capability ? {capability} : undefined);
45
+ const connectionsQuery = useIntegrationConnectionsQuery(workspaceId);
46
+ const updateConnection = useUpdateIntegrationConnectionMutation();
47
+ const deleteConnection = useDeleteIntegrationConnectionMutation();
48
+ const updateWebhookConnection = useUpdateWebhookConnectionMutation();
49
+ const deleteWebhookConnection = useDeleteWebhookConnectionMutation();
50
+
51
+ const providers = providersQuery.data?.providers ?? [];
52
+ const providersMap = new Map<string, IntegrationProviderDto>(
53
+ providers.map((provider) => [provider.provider, provider]),
54
+ );
55
+
56
+ const allConnections = connectionsQuery.data?.connections ?? [];
57
+ // Filter in memory so the all-status cache key stays shared; passing capability
58
+ // into the hook would collide with the active-only `source_control` key used
59
+ // elsewhere.
60
+ const connections = capability
61
+ ? allConnections.filter((connection) => connection.capabilities.includes(capability))
62
+ : allConnections;
63
+
64
+ const providerDisplayName = (provider: string) => providersMap.get(provider)?.display_name;
65
+ const providerLabel = (provider: string) => providerDisplayName(provider) ?? provider;
66
+
67
+ const sortedConnections = [...connections].sort((a, b) => {
68
+ const byProvider = providerLabel(a.provider).localeCompare(providerLabel(b.provider));
69
+ if (byProvider !== 0) return byProvider;
70
+ return a.created_at.localeCompare(b.created_at);
71
+ });
72
+ const usageConnection =
73
+ sortedConnections.find((connection) => connection.id === usageConnectionId) ??
74
+ (createdUsageConnection?.id === usageConnectionId ? createdUsageConnection : null) ??
75
+ null;
76
+ const deleteConnectionTarget = sortedConnections.find(
77
+ (connection) => connection.id === deleteConnectionId,
78
+ );
79
+
80
+ async function setConnectionActive(connection: IntegrationConnectionDto, active: boolean) {
81
+ try {
82
+ const body = {lifecycle_status: active ? 'active' : 'disabled'} as const;
83
+ if (connection.provider === 'webhook') {
84
+ await updateWebhookConnection.mutateAsync({
85
+ workspaceId,
86
+ connectionId: connection.id,
87
+ body,
88
+ });
89
+ } else {
90
+ await updateConnection.mutateAsync({
91
+ connectionId: connection.id,
92
+ body,
93
+ });
94
+ }
95
+ toast.success(active ? 'Integration enabled.' : 'Integration disabled.');
96
+ } catch (error) {
97
+ toast.error(error instanceof Error ? error.message : 'Could not update integration.');
98
+ }
99
+ }
100
+
101
+ async function confirmDeleteConnection() {
102
+ if (!deleteConnectionTarget) return;
103
+ try {
104
+ if (deleteConnectionTarget.provider === 'webhook') {
105
+ await deleteWebhookConnection.mutateAsync({
106
+ workspaceId,
107
+ connectionId: deleteConnectionTarget.id,
108
+ });
109
+ } else {
110
+ await deleteConnection.mutateAsync({
111
+ workspaceId,
112
+ connectionId: deleteConnectionTarget.id,
113
+ });
114
+ }
115
+ toast.success('Integration deleted.');
116
+ setDeleteConnectionId(undefined);
117
+ } catch (error) {
118
+ toast.error(error instanceof Error ? error.message : 'Could not delete integration.');
119
+ }
120
+ }
121
+
122
+ return (
123
+ <div className="flex flex-col gap-24">
124
+ <InstalledIntegrationsSection
125
+ connectionsQuery={connectionsQuery}
126
+ connections={sortedConnections}
127
+ isMutating={
128
+ updateConnection.isPending ||
129
+ deleteConnection.isPending ||
130
+ updateWebhookConnection.isPending ||
131
+ deleteWebhookConnection.isPending
132
+ }
133
+ onUse={setUsageConnectionId}
134
+ onSetActive={(connection, active) => {
135
+ void setConnectionActive(connection, active);
136
+ }}
137
+ onDelete={setDeleteConnectionId}
138
+ providerDisplayName={providerDisplayName}
139
+ />
140
+
141
+ <section className="flex flex-col gap-16" aria-label="Available integrations">
142
+ <div className="flex flex-col gap-4">
143
+ <Header variant="h3">Available integrations</Header>
144
+ <Text size="sm" className="text-foreground-neutral-muted">
145
+ Providers available to install in this workspace.
146
+ </Text>
147
+ </div>
148
+
149
+ <ProviderGrid
150
+ providersQuery={providersQuery}
151
+ workspaceId={workspaceId}
152
+ emptyMessage={emptyProvidersMessage}
153
+ onOpenProvider={setCreateProvider}
154
+ />
155
+ </section>
156
+ <WebhookCreateModal
157
+ workspaceId={workspaceId}
158
+ open={createProvider === 'webhook'}
159
+ onOpenChange={(open) => setCreateProvider(open ? 'webhook' : undefined)}
160
+ onCreated={(connection) => {
161
+ setCreatedUsageConnection(connection);
162
+ setUsageConnectionId(connection.id);
163
+ }}
164
+ />
165
+ <IntegrationUsageModal
166
+ connection={usageConnection}
167
+ events={usageConnection ? usageEventsForConnection(usageConnection) : []}
168
+ open={usageConnection !== null}
169
+ onOpenChange={(open) => {
170
+ if (!open) {
171
+ setUsageConnectionId(undefined);
172
+ setCreatedUsageConnection(undefined);
173
+ }
174
+ }}
175
+ >
176
+ {usageConnection?.provider === 'webhook' ? (
177
+ <WebhookUsageDetails workspaceId={workspaceId} connectionId={usageConnection.id} />
178
+ ) : null}
179
+ </IntegrationUsageModal>
180
+ <IntegrationDeleteConfirmModal
181
+ connectionName={deleteConnectionTarget?.display_name}
182
+ open={deleteConnectionId !== undefined}
183
+ isPending={deleteConnection.isPending || deleteWebhookConnection.isPending}
184
+ onOpenChange={(open) => {
185
+ if (!open) setDeleteConnectionId(undefined);
186
+ }}
187
+ onConfirm={() => {
188
+ void confirmDeleteConnection();
189
+ }}
190
+ />
191
+ </div>
192
+ );
193
+ }