@shipfox/client-integrations 21.0.0 → 22.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/components/callback-status-shell.d.ts.map +1 -1
- package/dist/components/callback-status-shell.js +4 -3
- package/dist/components/callback-status-shell.js.map +1 -1
- package/dist/components/connection-picker.js +2 -2
- package/dist/components/connection-picker.js.map +1 -1
- package/dist/components/installed-integrations-section.d.ts.map +1 -1
- package/dist/components/installed-integrations-section.js +155 -141
- package/dist/components/installed-integrations-section.js.map +1 -1
- package/dist/components/integration-gallery.stories.d.ts.map +1 -1
- package/dist/components/integration-gallery.stories.js +7 -2
- package/dist/components/integration-gallery.stories.js.map +1 -1
- package/dist/components/provider-grid.d.ts +0 -2
- package/dist/components/provider-grid.d.ts.map +1 -1
- package/dist/components/provider-grid.js +88 -104
- package/dist/components/provider-grid.js.map +1 -1
- package/dist/components/redirect-install-page.d.ts.map +1 -1
- package/dist/components/redirect-install-page.js +3 -2
- package/dist/components/redirect-install-page.js.map +1 -1
- package/dist/components/repository-picker.d.ts +1 -4
- package/dist/components/repository-picker.d.ts.map +1 -1
- package/dist/components/repository-picker.js +24 -37
- package/dist/components/repository-picker.js.map +1 -1
- package/dist/pages/gitea-install-page.js +1 -1
- package/dist/pages/gitea-install-page.js.map +1 -1
- package/dist/pages/jira-callback-page.js +4 -4
- package/dist/pages/jira-callback-page.js.map +1 -1
- package/dist/pages/sentry-callback-page.js +4 -4
- package/dist/pages/sentry-callback-page.js.map +1 -1
- package/dist/pages/source-control-onboarding-page.js +1 -1
- package/dist/pages/source-control-onboarding-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/components/callback-status-shell.tsx +4 -3
- package/src/components/connection-picker.tsx +2 -2
- package/src/components/installed-integrations-section.tsx +110 -98
- package/src/components/integration-gallery.stories.tsx +7 -2
- package/src/components/integration-gallery.test.tsx +3 -2
- package/src/components/provider-grid.tsx +69 -79
- package/src/components/redirect-install-page.test.tsx +4 -0
- package/src/components/redirect-install-page.tsx +3 -2
- package/src/components/repository-picker.test.tsx +27 -13
- package/src/components/repository-picker.tsx +22 -47
- package/src/pages/gitea-install-page.tsx +1 -1
- package/src/pages/jira-callback-page.tsx +4 -4
- package/src/pages/sentry-callback-page.tsx +3 -3
- package/src/pages/slack-callback-page.test.tsx +7 -0
- package/src/pages/source-control-onboarding-page.tsx +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-integrations",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "22.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"@shipfox/api-integration-sentry-dto": "12.2.0",
|
|
37
37
|
"@shipfox/api-integration-webhook-dto": "12.2.0",
|
|
38
38
|
"@shipfox/client-api": "6.0.1",
|
|
39
|
-
"@shipfox/client-auth": "
|
|
40
|
-
"@shipfox/client-shell": "
|
|
41
|
-
"@shipfox/client-ui": "
|
|
42
|
-
"@shipfox/integration-icons": "0.3.
|
|
43
|
-
"@shipfox/react-ui": "2.
|
|
39
|
+
"@shipfox/client-auth": "22.0.0",
|
|
40
|
+
"@shipfox/client-shell": "22.0.0",
|
|
41
|
+
"@shipfox/client-ui": "22.0.0",
|
|
42
|
+
"@shipfox/integration-icons": "0.3.5",
|
|
43
|
+
"@shipfox/react-ui": "2.1.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {FocusedFrame} from '@shipfox/client-shell/runtime';
|
|
1
2
|
import {ButtonLink} from '@shipfox/react-ui/button';
|
|
2
3
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
3
4
|
import {Text} from '@shipfox/react-ui/typography';
|
|
@@ -43,8 +44,8 @@ export function CallbackStatusShell({
|
|
|
43
44
|
: undefined;
|
|
44
45
|
|
|
45
46
|
return (
|
|
46
|
-
<main className="flex min-h-screen px-
|
|
47
|
-
<
|
|
47
|
+
<main className="flex min-h-screen px-frame py-frame">
|
|
48
|
+
<FocusedFrame className="flex flex-col justify-center gap-section">
|
|
48
49
|
<h2 ref={headingRef} tabIndex={-1} className="text-24 font-semibold outline-none">
|
|
49
50
|
{title}
|
|
50
51
|
</h2>
|
|
@@ -71,7 +72,7 @@ export function CallbackStatusShell({
|
|
|
71
72
|
) : null}
|
|
72
73
|
{settings}
|
|
73
74
|
</div>
|
|
74
|
-
</
|
|
75
|
+
</FocusedFrame>
|
|
75
76
|
</main>
|
|
76
77
|
);
|
|
77
78
|
}
|
|
@@ -19,15 +19,15 @@ export function ConnectionPicker({
|
|
|
19
19
|
const labelId = useId();
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<div className="flex flex-col
|
|
22
|
+
<div className="flex min-w-0 flex-col">
|
|
23
23
|
<Label id={labelId} className="sr-only">
|
|
24
24
|
Source integration
|
|
25
25
|
</Label>
|
|
26
26
|
<RadioGroup
|
|
27
|
+
variant="cell"
|
|
27
28
|
aria-labelledby={labelId}
|
|
28
29
|
value={selectedConnectionId ?? ''}
|
|
29
30
|
onValueChange={onSelect}
|
|
30
|
-
className="grid grid-cols-2 gap-inline max-[760px]:grid-cols-1"
|
|
31
31
|
>
|
|
32
32
|
{connections.map((connection) => (
|
|
33
33
|
<RadioGroupItem key={connection.id} value={connection.id}>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
DropdownMenuTrigger,
|
|
10
10
|
} from '@shipfox/react-ui/dropdown-menu';
|
|
11
11
|
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
12
|
+
import {Panel, PanelBody, PanelRow} from '@shipfox/react-ui/panel';
|
|
12
13
|
import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
13
14
|
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
14
15
|
import {cn, formatDate} from '@shipfox/react-ui/utils';
|
|
@@ -31,9 +32,6 @@ interface InstalledIntegrationsSectionProps {
|
|
|
31
32
|
providerDisplayName: (provider: string) => string | undefined;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const INSTALLED_SURFACE_CLASS =
|
|
35
|
-
'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
|
|
36
|
-
|
|
37
35
|
export function InstalledIntegrationsSection({
|
|
38
36
|
workspaceSlug,
|
|
39
37
|
connections,
|
|
@@ -59,39 +57,45 @@ export function InstalledIntegrationsSection({
|
|
|
59
57
|
{isPending ? <InstalledSkeleton label="Loading integrations" /> : null}
|
|
60
58
|
|
|
61
59
|
{error ? (
|
|
62
|
-
<
|
|
60
|
+
<Panel>
|
|
63
61
|
<QueryLoadError
|
|
64
62
|
query={{isError: true, isFetching, data: undefined, error, refetch: onRetry}}
|
|
65
63
|
subject="integrations"
|
|
64
|
+
variant="panel"
|
|
66
65
|
/>
|
|
67
|
-
</
|
|
66
|
+
</Panel>
|
|
68
67
|
) : null}
|
|
69
68
|
|
|
70
69
|
{!isPending && !error && connections.length === 0 ? (
|
|
71
|
-
<
|
|
70
|
+
<Panel>
|
|
72
71
|
<EmptyState
|
|
73
72
|
icon="componentLine"
|
|
74
73
|
title="No integrations installed yet"
|
|
75
74
|
description="Install a provider below to get started."
|
|
75
|
+
variant="panel"
|
|
76
76
|
/>
|
|
77
|
-
</
|
|
77
|
+
</Panel>
|
|
78
78
|
) : null}
|
|
79
79
|
|
|
80
80
|
{connections.length > 0 ? (
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
<Panel>
|
|
82
|
+
<PanelBody asChild>
|
|
83
|
+
<ul>
|
|
84
|
+
{connections.map((connection) => (
|
|
85
|
+
<InstalledRow
|
|
86
|
+
key={connection.id}
|
|
87
|
+
connection={connection}
|
|
88
|
+
workspaceSlug={workspaceSlug}
|
|
89
|
+
isMutating={isMutating}
|
|
90
|
+
onUse={onUse}
|
|
91
|
+
onSetActive={(nextActive) => onSetActive(connection, nextActive)}
|
|
92
|
+
onDelete={onDelete}
|
|
93
|
+
providerDisplayName={providerDisplayName}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</ul>
|
|
97
|
+
</PanelBody>
|
|
98
|
+
</Panel>
|
|
95
99
|
) : null}
|
|
96
100
|
</section>
|
|
97
101
|
);
|
|
@@ -120,91 +124,99 @@ function InstalledRow({
|
|
|
120
124
|
const providerName = providerDisplayName(connection.provider);
|
|
121
125
|
|
|
122
126
|
return (
|
|
123
|
-
<
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<div className="flex min-w-0
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
<PanelRow asChild className="justify-start gap-cluster">
|
|
128
|
+
<li>
|
|
129
|
+
<IntegrationIcon
|
|
130
|
+
source={connection.provider}
|
|
131
|
+
aria-hidden
|
|
132
|
+
className={cn(
|
|
133
|
+
'size-24 shrink-0',
|
|
134
|
+
muted ? 'text-foreground-neutral-disabled' : 'text-foreground-neutral-base',
|
|
135
|
+
)}
|
|
136
|
+
/>
|
|
137
|
+
<div className="flex min-w-0 flex-1 flex-col gap-tight">
|
|
138
|
+
<div className="flex min-w-0 items-center gap-inline">
|
|
139
|
+
<Text
|
|
140
|
+
size="md"
|
|
141
|
+
bold
|
|
142
|
+
className={cn('truncate', muted ? 'text-foreground-neutral-disabled' : undefined)}
|
|
143
|
+
>
|
|
144
|
+
{connection.displayName}
|
|
145
|
+
</Text>
|
|
146
|
+
<ConnectionStatusBadge status={connection.lifecycleStatus} className="shrink-0" />
|
|
147
|
+
</div>
|
|
148
|
+
<Text size="sm" className="truncate text-foreground-neutral-muted">
|
|
149
|
+
Added {formatDate(connection.createdAt)}
|
|
140
150
|
</Text>
|
|
141
|
-
<ConnectionStatusBadge status={connection.lifecycleStatus} className="shrink-0" />
|
|
142
151
|
</div>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
152
|
+
<DropdownMenu>
|
|
153
|
+
<DropdownMenuTrigger asChild>
|
|
154
|
+
<IconButton
|
|
155
|
+
size="sm"
|
|
156
|
+
variant="transparent"
|
|
157
|
+
icon="more2Line"
|
|
158
|
+
aria-label={`Open ${connection.displayName} integration actions`}
|
|
159
|
+
/>
|
|
160
|
+
</DropdownMenuTrigger>
|
|
161
|
+
<DropdownMenuContent align="end">
|
|
162
|
+
{connection.externalUrl ? (
|
|
163
|
+
<DropdownMenuItem asChild>
|
|
164
|
+
<a href={connection.externalUrl} target="_blank" rel="noreferrer noopener">
|
|
165
|
+
{providerName ? `Open in ${providerName}` : 'Open provider settings'}
|
|
166
|
+
</a>
|
|
167
|
+
</DropdownMenuItem>
|
|
168
|
+
) : null}
|
|
169
|
+
<DropdownMenuItem onSelect={() => onUse(connection.id)}>
|
|
170
|
+
Use this integration
|
|
171
|
+
</DropdownMenuItem>
|
|
172
|
+
{workspaceSlug ? (
|
|
173
|
+
<DropdownMenuItem asChild>
|
|
174
|
+
<Link
|
|
175
|
+
to="/w/$workspaceSlug/settings/events"
|
|
176
|
+
params={{workspaceSlug}}
|
|
177
|
+
search={{source: [connection.slug], event: [recentEventsEvent]}}
|
|
178
|
+
>
|
|
179
|
+
View recent events
|
|
180
|
+
</Link>
|
|
181
|
+
</DropdownMenuItem>
|
|
182
|
+
) : null}
|
|
183
|
+
<DropdownMenuSeparator />
|
|
184
|
+
<DropdownMenuItem disabled={isMutating} onSelect={() => onSetActive(!active)}>
|
|
185
|
+
{active ? 'Disable integration' : 'Enable integration'}
|
|
162
186
|
</DropdownMenuItem>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
Use this integration
|
|
166
|
-
</DropdownMenuItem>
|
|
167
|
-
{workspaceSlug ? (
|
|
168
|
-
<DropdownMenuItem asChild>
|
|
169
|
-
<Link
|
|
170
|
-
to="/w/$workspaceSlug/settings/events"
|
|
171
|
-
params={{workspaceSlug}}
|
|
172
|
-
search={{source: [connection.slug], event: [recentEventsEvent]}}
|
|
173
|
-
>
|
|
174
|
-
View recent events
|
|
175
|
-
</Link>
|
|
187
|
+
<DropdownMenuItem disabled={isMutating} onSelect={() => onDelete(connection.id)}>
|
|
188
|
+
Delete integration
|
|
176
189
|
</DropdownMenuItem>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
</DropdownMenuItem>
|
|
182
|
-
<DropdownMenuItem disabled={isMutating} onSelect={() => onDelete(connection.id)}>
|
|
183
|
-
Delete integration
|
|
184
|
-
</DropdownMenuItem>
|
|
185
|
-
</DropdownMenuContent>
|
|
186
|
-
</DropdownMenu>
|
|
187
|
-
</li>
|
|
190
|
+
</DropdownMenuContent>
|
|
191
|
+
</DropdownMenu>
|
|
192
|
+
</li>
|
|
193
|
+
</PanelRow>
|
|
188
194
|
);
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
function InstalledSkeleton({label}: {label: string}) {
|
|
192
198
|
return (
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
199
|
+
<Panel>
|
|
200
|
+
<PanelBody asChild>
|
|
201
|
+
<ul role="status" aria-label={label}>
|
|
202
|
+
{[0, 1, 2].map((row) => (
|
|
203
|
+
<PanelRow
|
|
204
|
+
asChild
|
|
205
|
+
className="justify-start gap-cluster hover:bg-background-neutral-base"
|
|
206
|
+
key={row}
|
|
207
|
+
>
|
|
208
|
+
<li>
|
|
209
|
+
<Skeleton className="size-24 shrink-0" />
|
|
210
|
+
<div className="flex min-w-0 flex-1 flex-col gap-tight">
|
|
211
|
+
<Skeleton className="h-16 w-120" />
|
|
212
|
+
<Skeleton className="h-12 w-80" />
|
|
213
|
+
</div>
|
|
214
|
+
<Skeleton className="h-20 w-72 shrink-0" />
|
|
215
|
+
</li>
|
|
216
|
+
</PanelRow>
|
|
217
|
+
))}
|
|
218
|
+
</ul>
|
|
219
|
+
</PanelBody>
|
|
220
|
+
</Panel>
|
|
209
221
|
);
|
|
210
222
|
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from '@tanstack/react-router';
|
|
18
18
|
import {createStore, Provider as JotaiProvider} from 'jotai';
|
|
19
19
|
import {useMemo} from 'react';
|
|
20
|
-
import {expect, screen, userEvent, within} from 'storybook/test';
|
|
20
|
+
import {expect, screen, userEvent, waitFor, within} from 'storybook/test';
|
|
21
21
|
import {IntegrationGallery} from './integration-gallery.js';
|
|
22
22
|
|
|
23
23
|
const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
|
|
@@ -162,7 +162,12 @@ export const UsageModalCloseRestoresInteraction: Story = {
|
|
|
162
162
|
await screen.findByRole('dialog', {name: 'Use acme-corp'});
|
|
163
163
|
await user.click(await screen.findByRole('button', {name: 'Done'}));
|
|
164
164
|
expect(screen.queryByRole('dialog', {name: 'Use acme-corp'})).not.toBeInTheDocument();
|
|
165
|
-
|
|
165
|
+
// The dialog unmounts before its dismissable layer releases the body, so
|
|
166
|
+
// wait for the release rather than asserting it on the same tick: clicking
|
|
167
|
+
// while `pointer-events: none` is still set is swallowed silently.
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
expect(document.body.style.pointerEvents).not.toBe('none');
|
|
170
|
+
});
|
|
166
171
|
await user.click(actionsButton);
|
|
167
172
|
await screen.findByRole('menuitem', {name: 'Use this integration'});
|
|
168
173
|
},
|
|
@@ -600,8 +600,9 @@ describe('IntegrationGallery: available section', () => {
|
|
|
600
600
|
|
|
601
601
|
const link = await screen.findByRole('link', {name: 'Install GitHub'});
|
|
602
602
|
|
|
603
|
-
|
|
604
|
-
|
|
603
|
+
// Cells run edge to edge inside the panel, so the ring is inset rather than
|
|
604
|
+
// the outset button ring the panel's `overflow-hidden` would crop.
|
|
605
|
+
expect(link).toHaveClass('focus-visible:shadow-focus-inset');
|
|
605
606
|
expect(link.className).not.toContain('shadow-button-secondary');
|
|
606
607
|
expect(within(link).getByText('Install')).toBeVisible();
|
|
607
608
|
expect(within(link).queryByRole('button')).not.toBeInTheDocument();
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import {QueryLoadError} from '@shipfox/client-ui';
|
|
2
2
|
import {IntegrationIcon} from '@shipfox/integration-icons';
|
|
3
3
|
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
4
|
-
import {
|
|
5
|
-
import {Panel} from '@shipfox/react-ui/panel';
|
|
4
|
+
import {Panel, PanelBody, PanelCell, PanelCellAction, PanelGrid} from '@shipfox/react-ui/panel';
|
|
6
5
|
import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
7
6
|
import {Text} from '@shipfox/react-ui/typography';
|
|
8
|
-
import {cn} from '@shipfox/react-ui/utils';
|
|
9
7
|
import {Link} from '@tanstack/react-router';
|
|
10
8
|
import type {IntegrationProvider} from '#core/models.js';
|
|
11
9
|
import {PROVIDER_CATALOG} from '#provider-catalog.js';
|
|
@@ -23,11 +21,6 @@ export interface ProviderGridProps {
|
|
|
23
21
|
onOpenProvider?: ((provider: string) => void) | undefined;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
export const PROVIDER_GRID_CLASS = 'grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1';
|
|
27
|
-
|
|
28
|
-
export const PROVIDER_SURFACE_CLASS =
|
|
29
|
-
'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
|
|
30
|
-
|
|
31
24
|
export function ProviderGrid({
|
|
32
25
|
workspaceSlug,
|
|
33
26
|
providers,
|
|
@@ -46,7 +39,7 @@ export function ProviderGrid({
|
|
|
46
39
|
|
|
47
40
|
if (error) {
|
|
48
41
|
return (
|
|
49
|
-
<
|
|
42
|
+
<Panel>
|
|
50
43
|
<QueryLoadError
|
|
51
44
|
query={{
|
|
52
45
|
isError: true,
|
|
@@ -56,39 +49,44 @@ export function ProviderGrid({
|
|
|
56
49
|
refetch: onRetry ?? (() => undefined),
|
|
57
50
|
}}
|
|
58
51
|
subject={errorSubject}
|
|
52
|
+
variant="panel"
|
|
59
53
|
/>
|
|
60
|
-
</
|
|
54
|
+
</Panel>
|
|
61
55
|
);
|
|
62
56
|
}
|
|
63
57
|
|
|
64
58
|
if (installableProviders.length === 0) {
|
|
65
59
|
return (
|
|
66
|
-
<
|
|
60
|
+
<Panel>
|
|
67
61
|
<EmptyState
|
|
68
62
|
icon="componentLine"
|
|
69
63
|
title="No integrations available"
|
|
70
64
|
description={emptyMessage}
|
|
65
|
+
variant="panel"
|
|
71
66
|
/>
|
|
72
|
-
</
|
|
67
|
+
</Panel>
|
|
73
68
|
);
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
return (
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
<Panel>
|
|
73
|
+
<PanelBody>
|
|
74
|
+
<PanelGrid aria-label="Available integrations">
|
|
75
|
+
{installableProviders.map((provider) => (
|
|
76
|
+
<ProviderCell
|
|
77
|
+
key={provider.provider}
|
|
78
|
+
provider={provider}
|
|
79
|
+
workspaceSlug={workspaceSlug}
|
|
80
|
+
onOpenProvider={onOpenProvider}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</PanelGrid>
|
|
84
|
+
</PanelBody>
|
|
85
|
+
</Panel>
|
|
88
86
|
);
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
function
|
|
89
|
+
function ProviderCell({
|
|
92
90
|
provider,
|
|
93
91
|
workspaceSlug,
|
|
94
92
|
onOpenProvider,
|
|
@@ -102,74 +100,66 @@ function ProviderCard({
|
|
|
102
100
|
|
|
103
101
|
if (catalog.kind === 'modal-connect') {
|
|
104
102
|
return (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
<PanelCell>
|
|
104
|
+
<PanelCellAction
|
|
105
|
+
action="Add"
|
|
106
|
+
aria-label={`Add ${provider.displayName}`}
|
|
107
|
+
onClick={() => onOpenProvider?.(provider.provider)}
|
|
108
|
+
>
|
|
109
|
+
<ProviderCellContent provider={provider} />
|
|
110
|
+
</PanelCellAction>
|
|
111
|
+
</PanelCell>
|
|
113
112
|
);
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
return (
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
<PanelCell>
|
|
117
|
+
<PanelCellAction asChild action="Install">
|
|
118
|
+
<Link
|
|
119
|
+
to={catalog.setupPath}
|
|
120
|
+
params={{workspaceSlug}}
|
|
121
|
+
aria-label={`Install ${provider.displayName}`}
|
|
122
|
+
>
|
|
123
|
+
<ProviderCellContent provider={provider} />
|
|
124
|
+
</Link>
|
|
125
|
+
</PanelCellAction>
|
|
126
|
+
</PanelCell>
|
|
125
127
|
);
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
function
|
|
129
|
-
provider,
|
|
130
|
-
action,
|
|
131
|
-
}: {
|
|
132
|
-
provider: IntegrationProvider;
|
|
133
|
-
action: 'Add' | 'Install';
|
|
134
|
-
}) {
|
|
130
|
+
function ProviderCellContent({provider}: {provider: IntegrationProvider}) {
|
|
135
131
|
return (
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</span>
|
|
147
|
-
<span className="flex shrink-0 items-center gap-tight text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
|
|
148
|
-
<Text as="span" size="sm">
|
|
149
|
-
{action}
|
|
150
|
-
</Text>
|
|
151
|
-
<Icon name="chevronRight" className="size-16" />
|
|
152
|
-
</span>
|
|
153
|
-
</>
|
|
132
|
+
<span className="flex min-w-0 items-center gap-cluster">
|
|
133
|
+
<IntegrationIcon
|
|
134
|
+
source={provider.provider}
|
|
135
|
+
aria-hidden
|
|
136
|
+
className="size-24 shrink-0 text-foreground-neutral-base"
|
|
137
|
+
/>
|
|
138
|
+
<Text as="span" size="md" bold className="truncate">
|
|
139
|
+
{provider.displayName}
|
|
140
|
+
</Text>
|
|
141
|
+
</span>
|
|
154
142
|
);
|
|
155
143
|
}
|
|
156
144
|
|
|
157
145
|
function ProviderGridSkeleton({label}: {label: string}) {
|
|
158
146
|
return (
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
<div className="flex
|
|
165
|
-
<
|
|
166
|
-
|
|
147
|
+
<Panel>
|
|
148
|
+
<PanelBody>
|
|
149
|
+
<PanelGrid role="status" aria-label={label}>
|
|
150
|
+
{[0, 1, 2, 3].map((tile) => (
|
|
151
|
+
<PanelCell key={tile}>
|
|
152
|
+
<div className="flex items-center justify-between gap-cluster px-row py-row">
|
|
153
|
+
<div className="flex min-w-0 items-center gap-cluster">
|
|
154
|
+
<Skeleton className="size-24 shrink-0" />
|
|
155
|
+
<Skeleton className="h-16 w-100" />
|
|
156
|
+
</div>
|
|
157
|
+
<Skeleton className="h-16 w-64 shrink-0" />
|
|
167
158
|
</div>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
</ul>
|
|
159
|
+
</PanelCell>
|
|
160
|
+
))}
|
|
161
|
+
</PanelGrid>
|
|
162
|
+
</PanelBody>
|
|
163
|
+
</Panel>
|
|
174
164
|
);
|
|
175
165
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import '@testing-library/jest-dom/vitest';
|
|
3
3
|
import {ApiError} from '@shipfox/client-api';
|
|
4
|
+
import {FOCUSED_FRAME_CONTENT_CLASS_NAME} from '@shipfox/client-shell/runtime';
|
|
4
5
|
import {screen, waitFor} from '@testing-library/react';
|
|
5
6
|
import {StrictMode} from 'react';
|
|
6
7
|
import {INTEGRATIONS_TEST_WID, renderIntegrationsPage} from '#test/render.js';
|
|
7
8
|
import {RedirectInstallPage} from './redirect-install-page.js';
|
|
8
9
|
|
|
10
|
+
const FOCUSED_FRAME_CONTENT_CLASSES = FOCUSED_FRAME_CONTENT_CLASS_NAME.split(' ');
|
|
11
|
+
|
|
9
12
|
function renderInstallPage(
|
|
10
13
|
props: Parameters<typeof RedirectInstallPage>[0],
|
|
11
14
|
options?: {strict?: boolean},
|
|
@@ -62,6 +65,7 @@ describe('RedirectInstallPage', () => {
|
|
|
62
65
|
|
|
63
66
|
// Alert mounts via framer-motion (opacity 0 in jsdom), so assert presence.
|
|
64
67
|
expect(await screen.findByText('Sentry app not configured')).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByRole('alert').parentElement).toHaveClass(...FOCUSED_FRAME_CONTENT_CLASSES);
|
|
65
69
|
expect(screen.getByRole('link', {name: 'Back to integrations'})).toBeVisible();
|
|
66
70
|
});
|
|
67
71
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {ApiError} from '@shipfox/client-api';
|
|
2
2
|
import {useActiveWorkspace} from '@shipfox/client-auth';
|
|
3
|
+
import {FocusedFrame} from '@shipfox/client-shell/runtime';
|
|
3
4
|
import {ButtonLink} from '@shipfox/react-ui/button';
|
|
4
5
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
5
6
|
import {FullPageLoader} from '@shipfox/react-ui/loader';
|
|
@@ -54,7 +55,7 @@ export function RedirectInstallPage({
|
|
|
54
55
|
|
|
55
56
|
if (errorMessage) {
|
|
56
57
|
return (
|
|
57
|
-
<
|
|
58
|
+
<FocusedFrame className="flex flex-col gap-group">
|
|
58
59
|
<Callout role="alert" type="error">
|
|
59
60
|
<Text size="sm">{errorMessage}</Text>
|
|
60
61
|
</Callout>
|
|
@@ -63,7 +64,7 @@ export function RedirectInstallPage({
|
|
|
63
64
|
Back to integrations
|
|
64
65
|
</Link>
|
|
65
66
|
</ButtonLink>
|
|
66
|
-
</
|
|
67
|
+
</FocusedFrame>
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
|