@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.
- package/.storybook/main.ts +1 -0
- package/.storybook/preview.css +4 -0
- package/.storybook/preview.tsx +56 -0
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +207 -0
- package/LICENSE +21 -0
- package/dist/components/connection-picker.d.ts +7 -0
- package/dist/components/connection-picker.d.ts.map +1 -0
- package/dist/components/connection-picker.js +98 -0
- package/dist/components/connection-picker.js.map +1 -0
- package/dist/components/installed-integrations-section.d.ts +14 -0
- package/dist/components/installed-integrations-section.d.ts.map +1 -0
- package/dist/components/installed-integrations-section.js +203 -0
- package/dist/components/installed-integrations-section.js.map +1 -0
- package/dist/components/integration-delete-confirm-modal.d.ts +10 -0
- package/dist/components/integration-delete-confirm-modal.d.ts.map +1 -0
- package/dist/components/integration-delete-confirm-modal.js +61 -0
- package/dist/components/integration-delete-confirm-modal.js.map +1 -0
- package/dist/components/integration-gallery-for-workspace.d.ts +9 -0
- package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -0
- package/dist/components/integration-gallery-for-workspace.js +169 -0
- package/dist/components/integration-gallery-for-workspace.js.map +1 -0
- package/dist/components/integration-gallery.d.ts +8 -0
- package/dist/components/integration-gallery.d.ts.map +1 -0
- package/dist/components/integration-gallery.js +26 -0
- package/dist/components/integration-gallery.js.map +1 -0
- package/dist/components/integration-gallery.stories.d.ts +26 -0
- package/dist/components/integration-gallery.stories.d.ts.map +1 -0
- package/dist/components/integration-gallery.stories.js +335 -0
- package/dist/components/integration-gallery.stories.js.map +1 -0
- package/dist/components/integration-usage-events.d.ts +4 -0
- package/dist/components/integration-usage-events.d.ts.map +1 -0
- package/dist/components/integration-usage-events.js +127 -0
- package/dist/components/integration-usage-events.js.map +1 -0
- package/dist/components/integration-usage-modal.d.ts +16 -0
- package/dist/components/integration-usage-modal.d.ts.map +1 -0
- package/dist/components/integration-usage-modal.js +184 -0
- package/dist/components/integration-usage-modal.js.map +1 -0
- package/dist/components/provider-grid.d.ts +14 -0
- package/dist/components/provider-grid.d.ts.map +1 -0
- package/dist/components/provider-grid.js +152 -0
- package/dist/components/provider-grid.js.map +1 -0
- package/dist/components/redirect-install-page.d.ts +19 -0
- package/dist/components/redirect-install-page.d.ts.map +1 -0
- package/dist/components/redirect-install-page.js +68 -0
- package/dist/components/redirect-install-page.js.map +1 -0
- package/dist/components/repository-picker.d.ts +15 -0
- package/dist/components/repository-picker.d.ts.map +1 -0
- package/dist/components/repository-picker.js +78 -0
- package/dist/components/repository-picker.js.map +1 -0
- package/dist/components/webhook/copyable-value.d.ts +10 -0
- package/dist/components/webhook/copyable-value.d.ts.map +1 -0
- package/dist/components/webhook/copyable-value.js +65 -0
- package/dist/components/webhook/copyable-value.js.map +1 -0
- package/dist/components/webhook/webhook-create-modal.d.ts +10 -0
- package/dist/components/webhook/webhook-create-modal.d.ts.map +1 -0
- package/dist/components/webhook/webhook-create-modal.js +233 -0
- package/dist/components/webhook/webhook-create-modal.js.map +1 -0
- package/dist/components/webhook/webhook-form-errors.d.ts +11 -0
- package/dist/components/webhook/webhook-form-errors.d.ts.map +1 -0
- package/dist/components/webhook/webhook-form-errors.js +27 -0
- package/dist/components/webhook/webhook-form-errors.js.map +1 -0
- package/dist/components/webhook/webhook-modals.stories.d.ts +25 -0
- package/dist/components/webhook/webhook-modals.stories.d.ts.map +1 -0
- package/dist/components/webhook/webhook-modals.stories.js +222 -0
- package/dist/components/webhook/webhook-modals.stories.js.map +1 -0
- package/dist/components/webhook/webhook-public-endpoint-alert.d.ts +2 -0
- package/dist/components/webhook/webhook-public-endpoint-alert.d.ts.map +1 -0
- package/dist/components/webhook/webhook-public-endpoint-alert.js +20 -0
- package/dist/components/webhook/webhook-public-endpoint-alert.js.map +1 -0
- package/dist/components/webhook/webhook-usage-details.d.ts +7 -0
- package/dist/components/webhook/webhook-usage-details.d.ts.map +1 -0
- package/dist/components/webhook/webhook-usage-details.js +49 -0
- package/dist/components/webhook/webhook-usage-details.js.map +1 -0
- package/dist/connection-status-badge.d.ts +12 -0
- package/dist/connection-status-badge.d.ts.map +1 -0
- package/dist/connection-status-badge.js +34 -0
- package/dist/connection-status-badge.js.map +1 -0
- package/dist/feature.d.ts +37 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/feature.js +48 -0
- package/dist/feature.js.map +1 -0
- package/dist/hooks/api/integrations.d.ts +265 -0
- package/dist/hooks/api/integrations.d.ts.map +1 -0
- package/dist/hooks/api/integrations.js +229 -0
- package/dist/hooks/api/integrations.js.map +1 -0
- package/dist/hooks/api/webhook-connections.d.ts +91 -0
- package/dist/hooks/api/webhook-connections.d.ts.map +1 -0
- package/dist/hooks/api/webhook-connections.js +97 -0
- package/dist/hooks/api/webhook-connections.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/integration-icon.d.ts +17 -0
- package/dist/integration-icon.d.ts.map +1 -0
- package/dist/integration-icon.js +22 -0
- package/dist/integration-icon.js.map +1 -0
- package/dist/linear-callback.d.ts +17 -0
- package/dist/linear-callback.d.ts.map +1 -0
- package/dist/linear-callback.js +113 -0
- package/dist/linear-callback.js.map +1 -0
- package/dist/pages/gitea-form-errors.d.ts +11 -0
- package/dist/pages/gitea-form-errors.d.ts.map +1 -0
- package/dist/pages/gitea-form-errors.js +35 -0
- package/dist/pages/gitea-form-errors.js.map +1 -0
- package/dist/pages/gitea-install-page.d.ts +2 -0
- package/dist/pages/gitea-install-page.d.ts.map +1 -0
- package/dist/pages/gitea-install-page.js +139 -0
- package/dist/pages/gitea-install-page.js.map +1 -0
- package/dist/pages/github-install-page.d.ts +2 -0
- package/dist/pages/github-install-page.d.ts.map +1 -0
- package/dist/pages/github-install-page.js +11 -0
- package/dist/pages/github-install-page.js.map +1 -0
- package/dist/pages/linear-callback-page.d.ts +2 -0
- package/dist/pages/linear-callback-page.d.ts.map +1 -0
- package/dist/pages/linear-callback-page.js +192 -0
- package/dist/pages/linear-callback-page.js.map +1 -0
- package/dist/pages/linear-install-page.d.ts +2 -0
- package/dist/pages/linear-install-page.d.ts.map +1 -0
- package/dist/pages/linear-install-page.js +24 -0
- package/dist/pages/linear-install-page.js.map +1 -0
- package/dist/pages/sentry-callback-page.d.ts +2 -0
- package/dist/pages/sentry-callback-page.d.ts.map +1 -0
- package/dist/pages/sentry-callback-page.js +256 -0
- package/dist/pages/sentry-callback-page.js.map +1 -0
- package/dist/pages/sentry-install-page.d.ts +2 -0
- package/dist/pages/sentry-install-page.d.ts.map +1 -0
- package/dist/pages/sentry-install-page.js +17 -0
- package/dist/pages/sentry-install-page.js.map +1 -0
- package/dist/pages/source-control-onboarding-page.d.ts +2 -0
- package/dist/pages/source-control-onboarding-page.d.ts.map +1 -0
- package/dist/pages/source-control-onboarding-page.js +37 -0
- package/dist/pages/source-control-onboarding-page.js.map +1 -0
- package/dist/provider-catalog.d.ts +15 -0
- package/dist/provider-catalog.d.ts.map +1 -0
- package/dist/provider-catalog.js +28 -0
- package/dist/provider-catalog.js.map +1 -0
- package/dist/routes/gitea.d.ts +6 -0
- package/dist/routes/gitea.d.ts.map +1 -0
- package/dist/routes/gitea.js +7 -0
- package/dist/routes/gitea.js.map +1 -0
- package/dist/routes/github-callback.d.ts +6 -0
- package/dist/routes/github-callback.d.ts.map +1 -0
- package/dist/routes/github-callback.js +129 -0
- package/dist/routes/github-callback.js.map +1 -0
- package/dist/routes/github.d.ts +6 -0
- package/dist/routes/github.d.ts.map +1 -0
- package/dist/routes/github.js +7 -0
- package/dist/routes/github.js.map +1 -0
- package/dist/routes/integrations.d.ts +6 -0
- package/dist/routes/integrations.d.ts.map +1 -0
- package/dist/routes/integrations.js +7 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/linear-callback.d.ts +6 -0
- package/dist/routes/linear-callback.d.ts.map +1 -0
- package/dist/routes/linear-callback.js +7 -0
- package/dist/routes/linear-callback.js.map +1 -0
- package/dist/routes/linear.d.ts +6 -0
- package/dist/routes/linear.d.ts.map +1 -0
- package/dist/routes/linear.js +7 -0
- package/dist/routes/linear.js.map +1 -0
- package/dist/routes/sentry-callback.d.ts +6 -0
- package/dist/routes/sentry-callback.d.ts.map +1 -0
- package/dist/routes/sentry-callback.js +7 -0
- package/dist/routes/sentry-callback.js.map +1 -0
- package/dist/routes/sentry.d.ts +6 -0
- package/dist/routes/sentry.d.ts.map +1 -0
- package/dist/routes/sentry.js +7 -0
- package/dist/routes/sentry.js.map +1 -0
- package/dist/sentry-callback.d.ts +35 -0
- package/dist/sentry-callback.d.ts.map +1 -0
- package/dist/sentry-callback.js +132 -0
- package/dist/sentry-callback.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +120 -0
- package/src/components/connection-picker.tsx +89 -0
- package/src/components/installed-integrations-section.tsx +198 -0
- package/src/components/integration-delete-confirm-modal.tsx +62 -0
- package/src/components/integration-gallery-for-workspace.tsx +193 -0
- package/src/components/integration-gallery.stories.tsx +302 -0
- package/src/components/integration-gallery.test.tsx +692 -0
- package/src/components/integration-gallery.tsx +49 -0
- package/src/components/integration-usage-events.test.ts +80 -0
- package/src/components/integration-usage-events.ts +116 -0
- package/src/components/integration-usage-modal.tsx +178 -0
- package/src/components/provider-grid.tsx +163 -0
- package/src/components/redirect-install-page.test.tsx +98 -0
- package/src/components/redirect-install-page.tsx +68 -0
- package/src/components/repository-picker.tsx +102 -0
- package/src/components/webhook/copyable-value.test.tsx +41 -0
- package/src/components/webhook/copyable-value.tsx +72 -0
- package/src/components/webhook/webhook-create-modal.tsx +247 -0
- package/src/components/webhook/webhook-form-errors.test.ts +45 -0
- package/src/components/webhook/webhook-form-errors.ts +32 -0
- package/src/components/webhook/webhook-modals.stories.tsx +225 -0
- package/src/components/webhook/webhook-public-endpoint-alert.tsx +15 -0
- package/src/components/webhook/webhook-usage-details.tsx +44 -0
- package/src/connection-status-badge.tsx +38 -0
- package/src/feature.ts +47 -0
- package/src/hooks/api/integrations.test.ts +105 -0
- package/src/hooks/api/integrations.ts +287 -0
- package/src/hooks/api/webhook-connections.ts +115 -0
- package/src/index.ts +16 -0
- package/src/integration-icon.react.test.tsx +40 -0
- package/src/integration-icon.test.ts +19 -0
- package/src/integration-icon.tsx +26 -0
- package/src/linear-callback.test.ts +227 -0
- package/src/linear-callback.ts +136 -0
- package/src/pages/gitea-form-errors.test.ts +55 -0
- package/src/pages/gitea-form-errors.ts +36 -0
- package/src/pages/gitea-install-page.tsx +108 -0
- package/src/pages/github-install-page.tsx +11 -0
- package/src/pages/linear-callback-page.test.tsx +157 -0
- package/src/pages/linear-callback-page.tsx +175 -0
- package/src/pages/linear-install-page.tsx +26 -0
- package/src/pages/sentry-callback-page.test.tsx +205 -0
- package/src/pages/sentry-callback-page.tsx +238 -0
- package/src/pages/sentry-install-page.tsx +17 -0
- package/src/pages/source-control-onboarding-page.test.tsx +46 -0
- package/src/pages/source-control-onboarding-page.tsx +26 -0
- package/src/provider-catalog.test.ts +42 -0
- package/src/provider-catalog.ts +49 -0
- package/src/routes/gitea.tsx +4 -0
- package/src/routes/github-callback.tsx +114 -0
- package/src/routes/github.tsx +4 -0
- package/src/routes/integrations.tsx +4 -0
- package/src/routes/linear-callback.tsx +4 -0
- package/src/routes/linear.tsx +4 -0
- package/src/routes/sentry-callback.tsx +4 -0
- package/src/routes/sentry.tsx +4 -0
- package/src/sentry-callback.test.ts +239 -0
- package/src/sentry-callback.ts +142 -0
- package/test/render.tsx +111 -0
- package/test/setup.ts +3 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vercel.json +8 -0
- package/vitest.config.ts +75 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRefreshAuth } from '@shipfox/client-auth';
|
|
3
|
+
import { ButtonLink } from '@shipfox/react-ui/button';
|
|
4
|
+
import { Callout } from '@shipfox/react-ui/callout';
|
|
5
|
+
import { FullPageLoader } from '@shipfox/react-ui/loader';
|
|
6
|
+
import { toast } from '@shipfox/react-ui/toast';
|
|
7
|
+
import { Text } from '@shipfox/react-ui/typography';
|
|
8
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { Link, useNavigate, useSearch } from '@tanstack/react-router';
|
|
10
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
+
import { integrationsQueryKeys, useCompleteLinearCallbackMutation } from '#hooks/api/integrations.js';
|
|
12
|
+
import { classifyLinearCallbackError, clearLinearInstallWorkspace, parseLinearCallbackQuery, readLinearInstallWorkspace, serializeLinearCallbackQuery } from '#linear-callback.js';
|
|
13
|
+
// Keyed by the callback's code+state. This page has no in-place Retry (failures
|
|
14
|
+
// route to "Start over", which begins a fresh install with a new state+code), so
|
|
15
|
+
// entries are intentionally never evicted: retaining them dedupes StrictMode and
|
|
16
|
+
// remount re-runs so the single-use grant code is exchanged at most once. The
|
|
17
|
+
// sibling Sentry page evicts on settle only because its Retry replays the code.
|
|
18
|
+
const callbackRequests = new Map();
|
|
19
|
+
// Keeps the success toast firing once per distinct callback even though the
|
|
20
|
+
// effect re-runs against the cached request as the mutation identity churns.
|
|
21
|
+
const toastedCallbacks = new Set();
|
|
22
|
+
export function LinearCallbackPage() {
|
|
23
|
+
const search = useSearch({
|
|
24
|
+
strict: false
|
|
25
|
+
});
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
const refreshAuth = useRefreshAuth();
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
const { mutateAsync: completeLinearCallback } = useCompleteLinearCallbackMutation();
|
|
30
|
+
const params = useMemo(()=>parseLinearCallbackQuery(search), [
|
|
31
|
+
search
|
|
32
|
+
]);
|
|
33
|
+
const workspaceId = useMemo(()=>{
|
|
34
|
+
try {
|
|
35
|
+
return readLinearInstallWorkspace(window.sessionStorage);
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
const [failure, setFailure] = useState();
|
|
41
|
+
useEffect(()=>{
|
|
42
|
+
if (!params) return;
|
|
43
|
+
let disposed = false;
|
|
44
|
+
const key = serializeLinearCallbackQuery(params);
|
|
45
|
+
let request = callbackRequests.get(key);
|
|
46
|
+
if (!request) {
|
|
47
|
+
request = refreshAuth().then(async (session)=>await completeLinearCallback({
|
|
48
|
+
query: params,
|
|
49
|
+
token: session.token
|
|
50
|
+
}));
|
|
51
|
+
callbackRequests.set(key, request);
|
|
52
|
+
}
|
|
53
|
+
request.then(async (connection)=>{
|
|
54
|
+
if (disposed) return;
|
|
55
|
+
try {
|
|
56
|
+
clearLinearInstallWorkspace(window.sessionStorage);
|
|
57
|
+
} catch {
|
|
58
|
+
// The successful API response remains the source of truth for navigation.
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await queryClient.invalidateQueries({
|
|
62
|
+
queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id)
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// Cache refresh is best effort: the successful callback is already committed server-side.
|
|
66
|
+
}
|
|
67
|
+
if (disposed) return;
|
|
68
|
+
if (!toastedCallbacks.has(key)) {
|
|
69
|
+
toastedCallbacks.add(key);
|
|
70
|
+
toast.success('Linear installed.');
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await navigate({
|
|
74
|
+
to: '/workspaces/$wid/settings/integrations',
|
|
75
|
+
params: {
|
|
76
|
+
wid: connection.workspace_id
|
|
77
|
+
},
|
|
78
|
+
replace: true
|
|
79
|
+
});
|
|
80
|
+
} catch {
|
|
81
|
+
// Keep the completed callback page visible if client navigation is interrupted.
|
|
82
|
+
}
|
|
83
|
+
}, (error)=>{
|
|
84
|
+
if (disposed) return;
|
|
85
|
+
setFailure(classifyLinearCallbackError(error));
|
|
86
|
+
});
|
|
87
|
+
return ()=>{
|
|
88
|
+
disposed = true;
|
|
89
|
+
};
|
|
90
|
+
}, [
|
|
91
|
+
completeLinearCallback,
|
|
92
|
+
navigate,
|
|
93
|
+
params,
|
|
94
|
+
queryClient,
|
|
95
|
+
refreshAuth
|
|
96
|
+
]);
|
|
97
|
+
if (!params) {
|
|
98
|
+
return /*#__PURE__*/ _jsx(LinearCallbackFailurePage, {
|
|
99
|
+
failure: {
|
|
100
|
+
title: 'Invalid Linear callback',
|
|
101
|
+
message: 'Invalid Linear callback. Start the install again from workspace settings.',
|
|
102
|
+
startOver: true,
|
|
103
|
+
signIn: false
|
|
104
|
+
},
|
|
105
|
+
workspaceId: workspaceId
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (!failure) return /*#__PURE__*/ _jsx(FullPageLoader, {
|
|
109
|
+
"aria-label": "Connecting Linear"
|
|
110
|
+
});
|
|
111
|
+
return /*#__PURE__*/ _jsx(LinearCallbackFailurePage, {
|
|
112
|
+
failure: failure,
|
|
113
|
+
workspaceId: workspaceId
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function LinearCallbackFailurePage({ failure, workspaceId }) {
|
|
117
|
+
const headingRef = useRef(null);
|
|
118
|
+
useEffect(()=>{
|
|
119
|
+
headingRef.current?.focus();
|
|
120
|
+
}, []);
|
|
121
|
+
const recoveryVariant = failure.startOver || failure.signIn ? 'muted' : 'base';
|
|
122
|
+
const switchAccountHref = workspaceId ? `/auth/logout?redirect=${encodeURIComponent(`/workspaces/${workspaceId}/integrations/linear`)}` : '/auth/logout';
|
|
123
|
+
const settingsLink = workspaceId ? /*#__PURE__*/ _jsx(ButtonLink, {
|
|
124
|
+
asChild: true,
|
|
125
|
+
variant: recoveryVariant,
|
|
126
|
+
className: "min-h-44 w-full sm:w-fit",
|
|
127
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
128
|
+
to: "/workspaces/$wid/settings/integrations",
|
|
129
|
+
params: {
|
|
130
|
+
wid: workspaceId
|
|
131
|
+
},
|
|
132
|
+
children: "Back to integrations"
|
|
133
|
+
})
|
|
134
|
+
}) : /*#__PURE__*/ _jsx(ButtonLink, {
|
|
135
|
+
asChild: true,
|
|
136
|
+
variant: recoveryVariant,
|
|
137
|
+
className: "min-h-44 w-full sm:w-fit",
|
|
138
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
139
|
+
to: "/",
|
|
140
|
+
children: "Back to Shipfox"
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
return /*#__PURE__*/ _jsx("main", {
|
|
144
|
+
className: "flex min-h-screen bg-background-subtle-base px-16 py-32",
|
|
145
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
146
|
+
className: "mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20",
|
|
147
|
+
children: [
|
|
148
|
+
/*#__PURE__*/ _jsx("h2", {
|
|
149
|
+
ref: headingRef,
|
|
150
|
+
tabIndex: -1,
|
|
151
|
+
className: "text-24 font-semibold outline-none",
|
|
152
|
+
children: failure.title ?? 'Linear install could not be completed'
|
|
153
|
+
}),
|
|
154
|
+
/*#__PURE__*/ _jsx(Callout, {
|
|
155
|
+
role: "alert",
|
|
156
|
+
type: "error",
|
|
157
|
+
children: /*#__PURE__*/ _jsx(Text, {
|
|
158
|
+
size: "sm",
|
|
159
|
+
children: failure.message
|
|
160
|
+
})
|
|
161
|
+
}),
|
|
162
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
163
|
+
className: "flex flex-col gap-8 sm:flex-row sm:items-center",
|
|
164
|
+
children: [
|
|
165
|
+
failure.signIn ? /*#__PURE__*/ _jsx(ButtonLink, {
|
|
166
|
+
asChild: true,
|
|
167
|
+
className: "min-h-44 w-full sm:w-fit",
|
|
168
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
169
|
+
to: switchAccountHref,
|
|
170
|
+
children: "Switch account"
|
|
171
|
+
})
|
|
172
|
+
}) : null,
|
|
173
|
+
failure.startOver && workspaceId ? /*#__PURE__*/ _jsx(ButtonLink, {
|
|
174
|
+
asChild: true,
|
|
175
|
+
className: "min-h-44 w-full sm:w-fit",
|
|
176
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
177
|
+
to: "/workspaces/$wid/integrations/linear",
|
|
178
|
+
params: {
|
|
179
|
+
wid: workspaceId
|
|
180
|
+
},
|
|
181
|
+
children: "Start over"
|
|
182
|
+
})
|
|
183
|
+
}) : null,
|
|
184
|
+
settingsLink
|
|
185
|
+
]
|
|
186
|
+
})
|
|
187
|
+
]
|
|
188
|
+
})
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=linear-callback-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pages/linear-callback-page.tsx"],"sourcesContent":["import type {LinearCallbackResponseDto} from '@shipfox/api-integration-linear-dto';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {Link, useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {integrationsQueryKeys, useCompleteLinearCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifyLinearCallbackError,\n clearLinearInstallWorkspace,\n type LinearCallbackFailure,\n parseLinearCallbackQuery,\n readLinearInstallWorkspace,\n serializeLinearCallbackQuery,\n} from '#linear-callback.js';\n\n// Keyed by the callback's code+state. This page has no in-place Retry (failures\n// route to \"Start over\", which begins a fresh install with a new state+code), so\n// entries are intentionally never evicted: retaining them dedupes StrictMode and\n// remount re-runs so the single-use grant code is exchanged at most once. The\n// sibling Sentry page evicts on settle only because its Retry replays the code.\nconst callbackRequests = new Map<string, Promise<LinearCallbackResponseDto>>();\n// Keeps the success toast firing once per distinct callback even though the\n// effect re-runs against the cached request as the mutation identity churns.\nconst toastedCallbacks = new Set<string>();\n\nexport function LinearCallbackPage() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const queryClient = useQueryClient();\n const {mutateAsync: completeLinearCallback} = useCompleteLinearCallbackMutation();\n const params = useMemo(() => parseLinearCallbackQuery(search), [search]);\n const workspaceId = useMemo(() => {\n try {\n return readLinearInstallWorkspace(window.sessionStorage);\n } catch {\n return undefined;\n }\n }, []);\n const [failure, setFailure] = useState<LinearCallbackFailure | undefined>();\n useEffect(() => {\n if (!params) return;\n\n let disposed = false;\n const key = serializeLinearCallbackQuery(params);\n let request = callbackRequests.get(key);\n if (!request) {\n request = refreshAuth().then(\n async (session) => await completeLinearCallback({query: params, token: session.token}),\n );\n callbackRequests.set(key, request);\n }\n\n request.then(\n async (connection) => {\n if (disposed) return;\n try {\n clearLinearInstallWorkspace(window.sessionStorage);\n } catch {\n // The successful API response remains the source of truth for navigation.\n }\n try {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),\n });\n } catch {\n // Cache refresh is best effort: the successful callback is already committed server-side.\n }\n if (disposed) return;\n if (!toastedCallbacks.has(key)) {\n toastedCallbacks.add(key);\n toast.success('Linear installed.');\n }\n try {\n await navigate({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: connection.workspace_id},\n replace: true,\n });\n } catch {\n // Keep the completed callback page visible if client navigation is interrupted.\n }\n },\n (error: unknown) => {\n if (disposed) return;\n setFailure(classifyLinearCallbackError(error));\n },\n );\n\n return () => {\n disposed = true;\n };\n }, [completeLinearCallback, navigate, params, queryClient, refreshAuth]);\n\n if (!params) {\n return (\n <LinearCallbackFailurePage\n failure={{\n title: 'Invalid Linear callback',\n message: 'Invalid Linear callback. Start the install again from workspace settings.',\n startOver: true,\n signIn: false,\n }}\n workspaceId={workspaceId}\n />\n );\n }\n\n if (!failure) return <FullPageLoader aria-label=\"Connecting Linear\" />;\n\n return <LinearCallbackFailurePage failure={failure} workspaceId={workspaceId} />;\n}\n\nfunction LinearCallbackFailurePage({\n failure,\n workspaceId,\n}: {\n failure: LinearCallbackFailure;\n workspaceId: string | undefined;\n}) {\n const headingRef = useRef<HTMLHeadingElement>(null);\n\n useEffect(() => {\n headingRef.current?.focus();\n }, []);\n\n const recoveryVariant = failure.startOver || failure.signIn ? 'muted' : 'base';\n const switchAccountHref = workspaceId\n ? `/auth/logout?redirect=${encodeURIComponent(`/workspaces/${workspaceId}/integrations/linear`)}`\n : '/auth/logout';\n const settingsLink = workspaceId ? (\n <ButtonLink asChild variant={recoveryVariant} className=\"min-h-44 w-full sm:w-fit\">\n <Link to=\"/workspaces/$wid/settings/integrations\" params={{wid: workspaceId}}>\n Back to integrations\n </Link>\n </ButtonLink>\n ) : (\n <ButtonLink asChild variant={recoveryVariant} className=\"min-h-44 w-full sm:w-fit\">\n <Link to=\"/\">Back to Shipfox</Link>\n </ButtonLink>\n );\n\n return (\n <main className=\"flex min-h-screen bg-background-subtle-base px-16 py-32\">\n <div className=\"mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20\">\n <h2 ref={headingRef} tabIndex={-1} className=\"text-24 font-semibold outline-none\">\n {failure.title ?? 'Linear install could not be completed'}\n </h2>\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">{failure.message}</Text>\n </Callout>\n <div className=\"flex flex-col gap-8 sm:flex-row sm:items-center\">\n {failure.signIn ? (\n <ButtonLink asChild className=\"min-h-44 w-full sm:w-fit\">\n <Link to={switchAccountHref}>Switch account</Link>\n </ButtonLink>\n ) : null}\n {failure.startOver && workspaceId ? (\n <ButtonLink asChild className=\"min-h-44 w-full sm:w-fit\">\n <Link to=\"/workspaces/$wid/integrations/linear\" params={{wid: workspaceId}}>\n Start over\n </Link>\n </ButtonLink>\n ) : null}\n {settingsLink}\n </div>\n </div>\n </main>\n );\n}\n"],"names":["useRefreshAuth","ButtonLink","Callout","FullPageLoader","toast","Text","useQueryClient","Link","useNavigate","useSearch","useEffect","useMemo","useRef","useState","integrationsQueryKeys","useCompleteLinearCallbackMutation","classifyLinearCallbackError","clearLinearInstallWorkspace","parseLinearCallbackQuery","readLinearInstallWorkspace","serializeLinearCallbackQuery","callbackRequests","Map","toastedCallbacks","Set","LinearCallbackPage","search","strict","navigate","refreshAuth","queryClient","mutateAsync","completeLinearCallback","params","workspaceId","window","sessionStorage","undefined","failure","setFailure","disposed","key","request","get","then","session","query","token","set","connection","invalidateQueries","queryKey","connectionsByWorkspace","workspace_id","has","add","success","to","wid","replace","error","LinearCallbackFailurePage","title","message","startOver","signIn","aria-label","headingRef","current","focus","recoveryVariant","switchAccountHref","encodeURIComponent","settingsLink","asChild","variant","className","main","div","h2","ref","tabIndex","role","type","size"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-install-page.d.ts","sourceRoot":"","sources":["../../src/pages/linear-install-page.tsx"],"names":[],"mappings":"AAKA,wBAAgB,iBAAiB,gCAoBhC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { RedirectInstallPage } from '#components/redirect-install-page.js';
|
|
4
|
+
import { useCreateLinearInstallMutation } from '#hooks/api/integrations.js';
|
|
5
|
+
import { saveLinearInstallWorkspace } from '#linear-callback.js';
|
|
6
|
+
export function LinearInstallPage() {
|
|
7
|
+
const createInstall = useCreateLinearInstallMutation();
|
|
8
|
+
const installRequest = useCallback(async (body)=>await createInstall.mutateAsync(body), [
|
|
9
|
+
createInstall
|
|
10
|
+
]);
|
|
11
|
+
return /*#__PURE__*/ _jsx(RedirectInstallPage, {
|
|
12
|
+
installRequest: installRequest,
|
|
13
|
+
errorFallbackMessage: "Could not start Linear install.",
|
|
14
|
+
beforeRedirect: (workspaceId)=>{
|
|
15
|
+
try {
|
|
16
|
+
saveLinearInstallWorkspace(window.sessionStorage, workspaceId);
|
|
17
|
+
} catch {
|
|
18
|
+
// Storage can be disabled before the helper gets a usable Storage object.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=linear-install-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pages/linear-install-page.tsx"],"sourcesContent":["import {useCallback} from 'react';\nimport {RedirectInstallPage} from '#components/redirect-install-page.js';\nimport {useCreateLinearInstallMutation} from '#hooks/api/integrations.js';\nimport {saveLinearInstallWorkspace} from '#linear-callback.js';\n\nexport function LinearInstallPage() {\n const createInstall = useCreateLinearInstallMutation();\n const installRequest = useCallback(\n async (body: {workspace_id: string}) => await createInstall.mutateAsync(body),\n [createInstall],\n );\n\n return (\n <RedirectInstallPage\n installRequest={installRequest}\n errorFallbackMessage=\"Could not start Linear install.\"\n beforeRedirect={(workspaceId) => {\n try {\n saveLinearInstallWorkspace(window.sessionStorage, workspaceId);\n } catch {\n // Storage can be disabled before the helper gets a usable Storage object.\n }\n }}\n />\n );\n}\n"],"names":["useCallback","RedirectInstallPage","useCreateLinearInstallMutation","saveLinearInstallWorkspace","LinearInstallPage","createInstall","installRequest","body","mutateAsync","errorFallbackMessage","beforeRedirect","workspaceId","window","sessionStorage"],"mappings":";AAAA,SAAQA,WAAW,QAAO,QAAQ;AAClC,SAAQC,mBAAmB,QAAO,uCAAuC;AACzE,SAAQC,8BAA8B,QAAO,6BAA6B;AAC1E,SAAQC,0BAA0B,QAAO,sBAAsB;AAE/D,OAAO,SAASC;IACd,MAAMC,gBAAgBH;IACtB,MAAMI,iBAAiBN,YACrB,OAAOO,OAAiC,MAAMF,cAAcG,WAAW,CAACD,OACxE;QAACF;KAAc;IAGjB,qBACE,KAACJ;QACCK,gBAAgBA;QAChBG,sBAAqB;QACrBC,gBAAgB,CAACC;YACf,IAAI;gBACFR,2BAA2BS,OAAOC,cAAc,EAAEF;YACpD,EAAE,OAAM;YACN,0EAA0E;YAC5E;QACF;;AAGN"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-callback-page.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,kBAAkB,uCAuMjC"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useAuthState, useRefreshAuth } from '@shipfox/client-auth';
|
|
3
|
+
import { Button, ButtonLink } from '@shipfox/react-ui/button';
|
|
4
|
+
import { Callout } from '@shipfox/react-ui/callout';
|
|
5
|
+
import { Card } from '@shipfox/react-ui/card';
|
|
6
|
+
import { FullPageLoader } from '@shipfox/react-ui/loader';
|
|
7
|
+
import { toast } from '@shipfox/react-ui/toast';
|
|
8
|
+
import { Header, Text } from '@shipfox/react-ui/typography';
|
|
9
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
10
|
+
import { Link, useNavigate, useSearch } from '@tanstack/react-router';
|
|
11
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
+
import { connectSentry, integrationsQueryKeys } from '#hooks/api/integrations.js';
|
|
13
|
+
import { classifySentryConnectError, clearSentryInstallWorkspace, parseSentryCallbackParams, preselectSentryWorkspace, readSentryInstallWorkspace } from '#sentry-callback.js';
|
|
14
|
+
// De-dupes concurrent attempts for the same installation+workspace+code
|
|
15
|
+
// (double click, remount while in flight). Entries are evicted once the
|
|
16
|
+
// request settles, so Retry, and any later attempt carrying a fresh grant
|
|
17
|
+
// code, always issues a genuinely new request instead of replaying a cached
|
|
18
|
+
// outcome.
|
|
19
|
+
const connectRequests = new Map();
|
|
20
|
+
export function SentryCallbackPage() {
|
|
21
|
+
const search = useSearch({
|
|
22
|
+
strict: false
|
|
23
|
+
});
|
|
24
|
+
const navigate = useNavigate();
|
|
25
|
+
const refreshAuth = useRefreshAuth();
|
|
26
|
+
const { workspaces, isLoading } = useAuthState();
|
|
27
|
+
const queryClient = useQueryClient();
|
|
28
|
+
const params = useMemo(()=>parseSentryCallbackParams(search), [
|
|
29
|
+
search
|
|
30
|
+
]);
|
|
31
|
+
const storedWorkspaceId = useMemo(()=>readSentryInstallWorkspace(window.sessionStorage), []);
|
|
32
|
+
const preselection = preselectSentryWorkspace(storedWorkspaceId, workspaces);
|
|
33
|
+
const preselectedId = preselection.kind === 'pick' ? preselection.preselectedId : undefined;
|
|
34
|
+
const [connectingId, setConnectingId] = useState();
|
|
35
|
+
const [failure, setFailure] = useState();
|
|
36
|
+
const [retryLocked, setRetryLocked] = useState(false);
|
|
37
|
+
const disposedRef = useRef(false);
|
|
38
|
+
useEffect(()=>{
|
|
39
|
+
disposedRef.current = false;
|
|
40
|
+
return ()=>{
|
|
41
|
+
disposedRef.current = true;
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
// Wait for auth before treating an empty workspace list as "no workspace": on
|
|
45
|
+
// a cold return from Sentry, `workspaces` is [] until the session refresh
|
|
46
|
+
// resolves, and bouncing to `/` then would discard the one-shot grant code.
|
|
47
|
+
const noWorkspace = !isLoading && preselection.kind === 'none';
|
|
48
|
+
useEffect(()=>{
|
|
49
|
+
if (!noWorkspace) return;
|
|
50
|
+
toast.error('You need a workspace before installing Sentry.');
|
|
51
|
+
navigate({
|
|
52
|
+
to: '/',
|
|
53
|
+
replace: true
|
|
54
|
+
});
|
|
55
|
+
}, [
|
|
56
|
+
noWorkspace,
|
|
57
|
+
navigate
|
|
58
|
+
]);
|
|
59
|
+
const retryAfterSeconds = failure?.failure.kind === 'retryable' ? failure.failure.retryAfterSeconds : undefined;
|
|
60
|
+
useEffect(()=>{
|
|
61
|
+
// No backoff window (any non-rate-limited failure) re-enables Retry, so a
|
|
62
|
+
// prior rate-limit lock can never strand the button in a disabled state.
|
|
63
|
+
if (!retryAfterSeconds) {
|
|
64
|
+
setRetryLocked(false);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setRetryLocked(true);
|
|
68
|
+
const timer = setTimeout(()=>setRetryLocked(false), retryAfterSeconds * 1000);
|
|
69
|
+
return ()=>clearTimeout(timer);
|
|
70
|
+
}, [
|
|
71
|
+
retryAfterSeconds
|
|
72
|
+
]);
|
|
73
|
+
if (!params) {
|
|
74
|
+
return /*#__PURE__*/ _jsxs(CallbackColumn, {
|
|
75
|
+
children: [
|
|
76
|
+
/*#__PURE__*/ _jsx(Callout, {
|
|
77
|
+
role: "alert",
|
|
78
|
+
type: "error",
|
|
79
|
+
children: /*#__PURE__*/ _jsx(Text, {
|
|
80
|
+
size: "sm",
|
|
81
|
+
children: "This Sentry link is missing required parameters. Start the install again from your workspace settings."
|
|
82
|
+
})
|
|
83
|
+
}),
|
|
84
|
+
/*#__PURE__*/ _jsx(ButtonLink, {
|
|
85
|
+
asChild: true,
|
|
86
|
+
variant: "muted",
|
|
87
|
+
className: "w-fit",
|
|
88
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
89
|
+
to: "/",
|
|
90
|
+
children: "Back to Shipfox"
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Hold the loader until auth resolves; the workspace list and the
|
|
97
|
+
// noWorkspace redirect both depend on a settled `workspaces`.
|
|
98
|
+
if (isLoading) return /*#__PURE__*/ _jsx(FullPageLoader, {});
|
|
99
|
+
if (noWorkspace) return null;
|
|
100
|
+
const connect = (workspaceId)=>{
|
|
101
|
+
setConnectingId(workspaceId);
|
|
102
|
+
setFailure(undefined);
|
|
103
|
+
const key = `${params.installationId}|${workspaceId}|${params.code}`;
|
|
104
|
+
let request = connectRequests.get(key);
|
|
105
|
+
if (!request) {
|
|
106
|
+
request = refreshAuth().then((session)=>connectSentry({
|
|
107
|
+
body: {
|
|
108
|
+
workspace_id: workspaceId,
|
|
109
|
+
code: params.code,
|
|
110
|
+
installation_id: params.installationId
|
|
111
|
+
},
|
|
112
|
+
token: session.token
|
|
113
|
+
}));
|
|
114
|
+
// Evict on settle (success or failure) so the key never replays a stale
|
|
115
|
+
// outcome. Two-arg `then` keeps this cleanup branch from surfacing the
|
|
116
|
+
// rejection as unhandled — the main chain below owns the user-facing
|
|
117
|
+
// failure handling.
|
|
118
|
+
const evict = ()=>connectRequests.delete(key);
|
|
119
|
+
request.then(evict, evict);
|
|
120
|
+
connectRequests.set(key, request);
|
|
121
|
+
}
|
|
122
|
+
request.then(async ()=>{
|
|
123
|
+
clearSentryInstallWorkspace(window.sessionStorage);
|
|
124
|
+
await queryClient.invalidateQueries({
|
|
125
|
+
queryKey: integrationsQueryKeys.connectionsByWorkspace(workspaceId)
|
|
126
|
+
});
|
|
127
|
+
if (disposedRef.current) return;
|
|
128
|
+
toast.success('Sentry installed.');
|
|
129
|
+
await navigate({
|
|
130
|
+
to: '/workspaces/$wid/settings/integrations',
|
|
131
|
+
params: {
|
|
132
|
+
wid: workspaceId
|
|
133
|
+
},
|
|
134
|
+
replace: true
|
|
135
|
+
});
|
|
136
|
+
}).catch((error)=>{
|
|
137
|
+
if (disposedRef.current) return;
|
|
138
|
+
const classified = classifySentryConnectError(error);
|
|
139
|
+
if (classified.kind === 'terminal') {
|
|
140
|
+
// Only a fresh install (new grant code) can recover; the stored
|
|
141
|
+
// handoff has served its purpose either way.
|
|
142
|
+
clearSentryInstallWorkspace(window.sessionStorage);
|
|
143
|
+
}
|
|
144
|
+
setConnectingId(undefined);
|
|
145
|
+
setFailure({
|
|
146
|
+
workspaceId,
|
|
147
|
+
failure: classified
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
const orderedWorkspaces = preselectedId ? [
|
|
152
|
+
...workspaces.filter((workspace)=>workspace.id === preselectedId),
|
|
153
|
+
...workspaces.filter((workspace)=>workspace.id !== preselectedId)
|
|
154
|
+
] : workspaces;
|
|
155
|
+
const failureWorkspaceId = failure?.workspaceId ?? preselectedId ?? workspaces[0]?.id;
|
|
156
|
+
return /*#__PURE__*/ _jsxs(CallbackColumn, {
|
|
157
|
+
children: [
|
|
158
|
+
/*#__PURE__*/ _jsxs("header", {
|
|
159
|
+
className: "flex flex-col gap-8",
|
|
160
|
+
children: [
|
|
161
|
+
/*#__PURE__*/ _jsx(Header, {
|
|
162
|
+
variant: "h2",
|
|
163
|
+
children: "Install Sentry"
|
|
164
|
+
}),
|
|
165
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
166
|
+
size: "sm",
|
|
167
|
+
className: "text-foreground-neutral-muted",
|
|
168
|
+
children: params.orgSlug ? `Install the Sentry org "${params.orgSlug}" in a workspace.` : 'Install this Sentry integration in a workspace.'
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
}),
|
|
172
|
+
failure ? /*#__PURE__*/ _jsx(Callout, {
|
|
173
|
+
role: "alert",
|
|
174
|
+
type: "error",
|
|
175
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
176
|
+
className: "flex flex-col gap-8",
|
|
177
|
+
children: [
|
|
178
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
179
|
+
size: "sm",
|
|
180
|
+
children: failure.failure.message
|
|
181
|
+
}),
|
|
182
|
+
failure.failure.kind === 'retryable' ? /*#__PURE__*/ _jsx(Button, {
|
|
183
|
+
size: "sm",
|
|
184
|
+
variant: "secondary",
|
|
185
|
+
className: "w-fit",
|
|
186
|
+
disabled: retryLocked || connectingId !== undefined,
|
|
187
|
+
onClick: ()=>connect(failure.workspaceId),
|
|
188
|
+
children: "Retry"
|
|
189
|
+
}) : null,
|
|
190
|
+
failure.failure.kind === 'terminal' && failure.failure.startOver ? /*#__PURE__*/ _jsx(Button, {
|
|
191
|
+
asChild: true,
|
|
192
|
+
size: "sm",
|
|
193
|
+
variant: "secondary",
|
|
194
|
+
className: "w-fit",
|
|
195
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
196
|
+
to: "/workspaces/$wid/integrations/sentry",
|
|
197
|
+
params: {
|
|
198
|
+
wid: failure.workspaceId
|
|
199
|
+
},
|
|
200
|
+
children: "Start over"
|
|
201
|
+
})
|
|
202
|
+
}) : null
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
}) : null,
|
|
206
|
+
/*#__PURE__*/ _jsx("section", {
|
|
207
|
+
className: "flex flex-col gap-8",
|
|
208
|
+
"aria-label": "Choose a workspace",
|
|
209
|
+
children: orderedWorkspaces.map((workspace)=>/*#__PURE__*/ _jsx(Card, {
|
|
210
|
+
className: "p-16",
|
|
211
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
212
|
+
className: "flex items-center justify-between gap-12",
|
|
213
|
+
children: [
|
|
214
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
215
|
+
size: "md",
|
|
216
|
+
bold: true,
|
|
217
|
+
className: "truncate",
|
|
218
|
+
children: workspace.name
|
|
219
|
+
}),
|
|
220
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
221
|
+
variant: "secondary",
|
|
222
|
+
disabled: connectingId !== undefined,
|
|
223
|
+
isLoading: connectingId === workspace.id,
|
|
224
|
+
onClick: ()=>connect(workspace.id),
|
|
225
|
+
children: "Install"
|
|
226
|
+
})
|
|
227
|
+
]
|
|
228
|
+
})
|
|
229
|
+
}, workspace.id))
|
|
230
|
+
}),
|
|
231
|
+
failureWorkspaceId ? /*#__PURE__*/ _jsx(ButtonLink, {
|
|
232
|
+
asChild: true,
|
|
233
|
+
variant: "muted",
|
|
234
|
+
className: "w-fit",
|
|
235
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
236
|
+
to: "/workspaces/$wid/settings/integrations",
|
|
237
|
+
params: {
|
|
238
|
+
wid: failureWorkspaceId
|
|
239
|
+
},
|
|
240
|
+
children: "Back to settings"
|
|
241
|
+
})
|
|
242
|
+
}) : null
|
|
243
|
+
]
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function CallbackColumn({ children }) {
|
|
247
|
+
return /*#__PURE__*/ _jsx("main", {
|
|
248
|
+
className: "flex min-h-screen bg-background-subtle-base px-16 py-32",
|
|
249
|
+
children: /*#__PURE__*/ _jsx("div", {
|
|
250
|
+
className: "mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20",
|
|
251
|
+
children: children
|
|
252
|
+
})
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
//# sourceMappingURL=sentry-callback-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pages/sentry-callback-page.tsx"],"sourcesContent":["import type {SentryConnectResponseDto} from '@shipfox/api-integration-sentry-dto';\nimport {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {Link, useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {connectSentry, integrationsQueryKeys} from '#hooks/api/integrations.js';\nimport {\n classifySentryConnectError,\n clearSentryInstallWorkspace,\n parseSentryCallbackParams,\n preselectSentryWorkspace,\n readSentryInstallWorkspace,\n type SentryConnectFailure,\n} from '#sentry-callback.js';\n\n// De-dupes concurrent attempts for the same installation+workspace+code\n// (double click, remount while in flight). Entries are evicted once the\n// request settles, so Retry, and any later attempt carrying a fresh grant\n// code, always issues a genuinely new request instead of replaying a cached\n// outcome.\nconst connectRequests = new Map<string, Promise<SentryConnectResponseDto>>();\n\nexport function SentryCallbackPage() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const {workspaces, isLoading} = useAuthState();\n const queryClient = useQueryClient();\n\n const params = useMemo(() => parseSentryCallbackParams(search), [search]);\n const storedWorkspaceId = useMemo(() => readSentryInstallWorkspace(window.sessionStorage), []);\n const preselection = preselectSentryWorkspace(storedWorkspaceId, workspaces);\n const preselectedId = preselection.kind === 'pick' ? preselection.preselectedId : undefined;\n\n const [connectingId, setConnectingId] = useState<string | undefined>();\n const [failure, setFailure] = useState<\n {workspaceId: string; failure: SentryConnectFailure} | undefined\n >();\n const [retryLocked, setRetryLocked] = useState(false);\n const disposedRef = useRef(false);\n\n useEffect(() => {\n disposedRef.current = false;\n return () => {\n disposedRef.current = true;\n };\n }, []);\n\n // Wait for auth before treating an empty workspace list as \"no workspace\": on\n // a cold return from Sentry, `workspaces` is [] until the session refresh\n // resolves, and bouncing to `/` then would discard the one-shot grant code.\n const noWorkspace = !isLoading && preselection.kind === 'none';\n useEffect(() => {\n if (!noWorkspace) return;\n toast.error('You need a workspace before installing Sentry.');\n navigate({to: '/', replace: true});\n }, [noWorkspace, navigate]);\n\n const retryAfterSeconds =\n failure?.failure.kind === 'retryable' ? failure.failure.retryAfterSeconds : undefined;\n useEffect(() => {\n // No backoff window (any non-rate-limited failure) re-enables Retry, so a\n // prior rate-limit lock can never strand the button in a disabled state.\n if (!retryAfterSeconds) {\n setRetryLocked(false);\n return;\n }\n setRetryLocked(true);\n const timer = setTimeout(() => setRetryLocked(false), retryAfterSeconds * 1000);\n return () => clearTimeout(timer);\n }, [retryAfterSeconds]);\n\n if (!params) {\n return (\n <CallbackColumn>\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">\n This Sentry link is missing required parameters. Start the install again from your\n workspace settings.\n </Text>\n </Callout>\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/\">Back to Shipfox</Link>\n </ButtonLink>\n </CallbackColumn>\n );\n }\n\n // Hold the loader until auth resolves; the workspace list and the\n // noWorkspace redirect both depend on a settled `workspaces`.\n if (isLoading) return <FullPageLoader />;\n\n if (noWorkspace) return null;\n\n const connect = (workspaceId: string) => {\n setConnectingId(workspaceId);\n setFailure(undefined);\n const key = `${params.installationId}|${workspaceId}|${params.code}`;\n let request = connectRequests.get(key);\n if (!request) {\n request = refreshAuth().then((session) =>\n connectSentry({\n body: {\n workspace_id: workspaceId,\n code: params.code,\n installation_id: params.installationId,\n },\n token: session.token,\n }),\n );\n // Evict on settle (success or failure) so the key never replays a stale\n // outcome. Two-arg `then` keeps this cleanup branch from surfacing the\n // rejection as unhandled — the main chain below owns the user-facing\n // failure handling.\n const evict = () => connectRequests.delete(key);\n request.then(evict, evict);\n connectRequests.set(key, request);\n }\n\n request\n .then(async () => {\n clearSentryInstallWorkspace(window.sessionStorage);\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(workspaceId),\n });\n if (disposedRef.current) return;\n toast.success('Sentry installed.');\n await navigate({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: workspaceId},\n replace: true,\n });\n })\n .catch((error: unknown) => {\n if (disposedRef.current) return;\n const classified = classifySentryConnectError(error);\n if (classified.kind === 'terminal') {\n // Only a fresh install (new grant code) can recover; the stored\n // handoff has served its purpose either way.\n clearSentryInstallWorkspace(window.sessionStorage);\n }\n setConnectingId(undefined);\n setFailure({workspaceId, failure: classified});\n });\n };\n\n const orderedWorkspaces = preselectedId\n ? [\n ...workspaces.filter((workspace) => workspace.id === preselectedId),\n ...workspaces.filter((workspace) => workspace.id !== preselectedId),\n ]\n : workspaces;\n\n const failureWorkspaceId = failure?.workspaceId ?? preselectedId ?? workspaces[0]?.id;\n\n return (\n <CallbackColumn>\n <header className=\"flex flex-col gap-8\">\n <Header variant=\"h2\">Install Sentry</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n {params.orgSlug\n ? `Install the Sentry org \"${params.orgSlug}\" in a workspace.`\n : 'Install this Sentry integration in a workspace.'}\n </Text>\n </header>\n\n {failure ? (\n <Callout role=\"alert\" type=\"error\">\n <div className=\"flex flex-col gap-8\">\n <Text size=\"sm\">{failure.failure.message}</Text>\n {failure.failure.kind === 'retryable' ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n disabled={retryLocked || connectingId !== undefined}\n onClick={() => connect(failure.workspaceId)}\n >\n Retry\n </Button>\n ) : null}\n {failure.failure.kind === 'terminal' && failure.failure.startOver ? (\n <Button asChild size=\"sm\" variant=\"secondary\" className=\"w-fit\">\n <Link to=\"/workspaces/$wid/integrations/sentry\" params={{wid: failure.workspaceId}}>\n Start over\n </Link>\n </Button>\n ) : null}\n </div>\n </Callout>\n ) : null}\n\n <section className=\"flex flex-col gap-8\" aria-label=\"Choose a workspace\">\n {orderedWorkspaces.map((workspace) => (\n <Card key={workspace.id} className=\"p-16\">\n <div className=\"flex items-center justify-between gap-12\">\n <Text size=\"md\" bold className=\"truncate\">\n {workspace.name}\n </Text>\n <Button\n variant=\"secondary\"\n disabled={connectingId !== undefined}\n isLoading={connectingId === workspace.id}\n onClick={() => connect(workspace.id)}\n >\n Install\n </Button>\n </div>\n </Card>\n ))}\n </section>\n\n {failureWorkspaceId ? (\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/workspaces/$wid/settings/integrations\" params={{wid: failureWorkspaceId}}>\n Back to settings\n </Link>\n </ButtonLink>\n ) : null}\n </CallbackColumn>\n );\n}\n\nfunction CallbackColumn({children}: {children: React.ReactNode}) {\n return (\n <main className=\"flex min-h-screen bg-background-subtle-base px-16 py-32\">\n <div className=\"mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20\">\n {children}\n </div>\n </main>\n );\n}\n"],"names":["useAuthState","useRefreshAuth","Button","ButtonLink","Callout","Card","FullPageLoader","toast","Header","Text","useQueryClient","Link","useNavigate","useSearch","useEffect","useMemo","useRef","useState","connectSentry","integrationsQueryKeys","classifySentryConnectError","clearSentryInstallWorkspace","parseSentryCallbackParams","preselectSentryWorkspace","readSentryInstallWorkspace","connectRequests","Map","SentryCallbackPage","search","strict","navigate","refreshAuth","workspaces","isLoading","queryClient","params","storedWorkspaceId","window","sessionStorage","preselection","preselectedId","kind","undefined","connectingId","setConnectingId","failure","setFailure","retryLocked","setRetryLocked","disposedRef","current","noWorkspace","error","to","replace","retryAfterSeconds","timer","setTimeout","clearTimeout","CallbackColumn","role","type","size","asChild","variant","className","connect","workspaceId","key","installationId","code","request","get","then","session","body","workspace_id","installation_id","token","evict","delete","set","invalidateQueries","queryKey","connectionsByWorkspace","success","wid","catch","classified","orderedWorkspaces","filter","workspace","id","failureWorkspaceId","header","orgSlug","div","message","disabled","onClick","startOver","section","aria-label","map","bold","name","children","main"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry-install-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-install-page.tsx"],"names":[],"mappings":"AAIA,wBAAgB,iBAAiB,gCAYhC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { RedirectInstallPage } from '#components/redirect-install-page.js';
|
|
3
|
+
import { createSentryInstall } from '#hooks/api/integrations.js';
|
|
4
|
+
import { saveSentryInstallWorkspace } from '#sentry-callback.js';
|
|
5
|
+
export function SentryInstallPage() {
|
|
6
|
+
return /*#__PURE__*/ _jsx(RedirectInstallPage, {
|
|
7
|
+
installRequest: createSentryInstall,
|
|
8
|
+
errorFallbackMessage: "Could not start Sentry install.",
|
|
9
|
+
// Sentry's redirect has no state param; the stored id lets the callback
|
|
10
|
+
// pre-select this workspace. Saving never throws (see helper).
|
|
11
|
+
beforeRedirect: (workspaceId)=>{
|
|
12
|
+
saveSentryInstallWorkspace(window.sessionStorage, workspaceId);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=sentry-install-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pages/sentry-install-page.tsx"],"sourcesContent":["import {RedirectInstallPage} from '#components/redirect-install-page.js';\nimport {createSentryInstall} from '#hooks/api/integrations.js';\nimport {saveSentryInstallWorkspace} from '#sentry-callback.js';\n\nexport function SentryInstallPage() {\n return (\n <RedirectInstallPage\n installRequest={createSentryInstall}\n errorFallbackMessage=\"Could not start Sentry install.\"\n // Sentry's redirect has no state param; the stored id lets the callback\n // pre-select this workspace. Saving never throws (see helper).\n beforeRedirect={(workspaceId) => {\n saveSentryInstallWorkspace(window.sessionStorage, workspaceId);\n }}\n />\n );\n}\n"],"names":["RedirectInstallPage","createSentryInstall","saveSentryInstallWorkspace","SentryInstallPage","installRequest","errorFallbackMessage","beforeRedirect","workspaceId","window","sessionStorage"],"mappings":";AAAA,SAAQA,mBAAmB,QAAO,uCAAuC;AACzE,SAAQC,mBAAmB,QAAO,6BAA6B;AAC/D,SAAQC,0BAA0B,QAAO,sBAAsB;AAE/D,OAAO,SAASC;IACd,qBACE,KAACH;QACCI,gBAAgBH;QAChBI,sBAAqB;QACrB,wEAAwE;QACxE,+DAA+D;QAC/DC,gBAAgB,CAACC;YACfL,2BAA2BM,OAAOC,cAAc,EAAEF;QACpD;;AAGN"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-control-onboarding-page.d.ts","sourceRoot":"","sources":["../../src/pages/source-control-onboarding-page.tsx"],"names":[],"mappings":"AAKA,wBAAgB,2BAA2B,gCAoB1C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useActiveWorkspace } from '@shipfox/client-auth';
|
|
3
|
+
import { Header, Text } from '@shipfox/react-ui/typography';
|
|
4
|
+
import { ProviderGrid } from '#components/provider-grid.js';
|
|
5
|
+
import { useIntegrationProvidersQuery } from '#hooks/api/integrations.js';
|
|
6
|
+
export function SourceControlOnboardingPage() {
|
|
7
|
+
const workspace = useActiveWorkspace();
|
|
8
|
+
const providersQuery = useIntegrationProvidersQuery({
|
|
9
|
+
capability: 'source_control'
|
|
10
|
+
});
|
|
11
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
12
|
+
className: "mx-auto flex w-full max-w-[640px] flex-col gap-20",
|
|
13
|
+
children: [
|
|
14
|
+
/*#__PURE__*/ _jsxs("header", {
|
|
15
|
+
className: "flex flex-col gap-8",
|
|
16
|
+
children: [
|
|
17
|
+
/*#__PURE__*/ _jsx(Header, {
|
|
18
|
+
variant: "h1",
|
|
19
|
+
children: "Install source control"
|
|
20
|
+
}),
|
|
21
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
22
|
+
size: "md",
|
|
23
|
+
className: "text-foreground-neutral-muted",
|
|
24
|
+
children: "Shipfox needs a source control integration to import your repositories."
|
|
25
|
+
})
|
|
26
|
+
]
|
|
27
|
+
}),
|
|
28
|
+
/*#__PURE__*/ _jsx(ProviderGrid, {
|
|
29
|
+
providersQuery: providersQuery,
|
|
30
|
+
workspaceId: workspace.id,
|
|
31
|
+
emptyMessage: "Enable at least one source-control provider in the application settings."
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=source-control-onboarding-page.js.map
|