@shipfox/client-integrations 21.0.0 → 22.0.1
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 +28 -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 +160 -153
- package/dist/components/installed-integrations-section.js.map +1 -1
- package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -1
- package/dist/components/integration-gallery-for-workspace.js +5 -12
- package/dist/components/integration-gallery-for-workspace.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/routes/integrations-settings.d.ts +1 -2
- package/dist/routes/integrations-settings.d.ts.map +1 -1
- package/dist/routes/integrations-settings.js +12 -1
- package/dist/routes/integrations-settings.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 -101
- package/src/components/integration-gallery-for-workspace.tsx +0 -3
- package/src/components/integration-gallery.stories.tsx +7 -2
- package/src/components/integration-gallery.test.tsx +9 -4
- 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/src/routes/integrations-settings.tsx +7 -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.1",
|
|
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.1",
|
|
40
|
+
"@shipfox/client-shell": "22.0.1",
|
|
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,
|
|
@@ -51,47 +49,50 @@ export function InstalledIntegrationsSection({
|
|
|
51
49
|
<section className="flex flex-col gap-group" aria-label="Installed integrations">
|
|
52
50
|
<div className="flex flex-col gap-tight">
|
|
53
51
|
<Header variant="h3">Installed integrations</Header>
|
|
54
|
-
<Text size="sm" className="text-foreground-neutral-muted">
|
|
55
|
-
Provider accounts installed in this workspace.
|
|
56
|
-
</Text>
|
|
57
52
|
</div>
|
|
58
53
|
|
|
59
54
|
{isPending ? <InstalledSkeleton label="Loading integrations" /> : null}
|
|
60
55
|
|
|
61
56
|
{error ? (
|
|
62
|
-
<
|
|
57
|
+
<Panel>
|
|
63
58
|
<QueryLoadError
|
|
64
59
|
query={{isError: true, isFetching, data: undefined, error, refetch: onRetry}}
|
|
65
60
|
subject="integrations"
|
|
61
|
+
variant="panel"
|
|
66
62
|
/>
|
|
67
|
-
</
|
|
63
|
+
</Panel>
|
|
68
64
|
) : null}
|
|
69
65
|
|
|
70
66
|
{!isPending && !error && connections.length === 0 ? (
|
|
71
|
-
<
|
|
67
|
+
<Panel>
|
|
72
68
|
<EmptyState
|
|
73
69
|
icon="componentLine"
|
|
74
70
|
title="No integrations installed yet"
|
|
75
71
|
description="Install a provider below to get started."
|
|
72
|
+
variant="panel"
|
|
76
73
|
/>
|
|
77
|
-
</
|
|
74
|
+
</Panel>
|
|
78
75
|
) : null}
|
|
79
76
|
|
|
80
77
|
{connections.length > 0 ? (
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
<Panel>
|
|
79
|
+
<PanelBody asChild>
|
|
80
|
+
<ul>
|
|
81
|
+
{connections.map((connection) => (
|
|
82
|
+
<InstalledRow
|
|
83
|
+
key={connection.id}
|
|
84
|
+
connection={connection}
|
|
85
|
+
workspaceSlug={workspaceSlug}
|
|
86
|
+
isMutating={isMutating}
|
|
87
|
+
onUse={onUse}
|
|
88
|
+
onSetActive={(nextActive) => onSetActive(connection, nextActive)}
|
|
89
|
+
onDelete={onDelete}
|
|
90
|
+
providerDisplayName={providerDisplayName}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
</PanelBody>
|
|
95
|
+
</Panel>
|
|
95
96
|
) : null}
|
|
96
97
|
</section>
|
|
97
98
|
);
|
|
@@ -120,91 +121,99 @@ function InstalledRow({
|
|
|
120
121
|
const providerName = providerDisplayName(connection.provider);
|
|
121
122
|
|
|
122
123
|
return (
|
|
123
|
-
<
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<div className="flex min-w-0
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
124
|
+
<PanelRow asChild className="justify-start gap-cluster">
|
|
125
|
+
<li>
|
|
126
|
+
<IntegrationIcon
|
|
127
|
+
source={connection.provider}
|
|
128
|
+
aria-hidden
|
|
129
|
+
className={cn(
|
|
130
|
+
'size-24 shrink-0',
|
|
131
|
+
muted ? 'text-foreground-neutral-disabled' : 'text-foreground-neutral-base',
|
|
132
|
+
)}
|
|
133
|
+
/>
|
|
134
|
+
<div className="flex min-w-0 flex-1 flex-col gap-tight">
|
|
135
|
+
<div className="flex min-w-0 items-center gap-inline">
|
|
136
|
+
<Text
|
|
137
|
+
size="md"
|
|
138
|
+
bold
|
|
139
|
+
className={cn('truncate', muted ? 'text-foreground-neutral-disabled' : undefined)}
|
|
140
|
+
>
|
|
141
|
+
{connection.displayName}
|
|
142
|
+
</Text>
|
|
143
|
+
<ConnectionStatusBadge status={connection.lifecycleStatus} className="shrink-0" />
|
|
144
|
+
</div>
|
|
145
|
+
<Text size="sm" className="truncate text-foreground-neutral-muted">
|
|
146
|
+
Added {formatDate(connection.createdAt)}
|
|
140
147
|
</Text>
|
|
141
|
-
<ConnectionStatusBadge status={connection.lifecycleStatus} className="shrink-0" />
|
|
142
148
|
</div>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
149
|
+
<DropdownMenu>
|
|
150
|
+
<DropdownMenuTrigger asChild>
|
|
151
|
+
<IconButton
|
|
152
|
+
size="sm"
|
|
153
|
+
variant="transparent"
|
|
154
|
+
icon="more2Line"
|
|
155
|
+
aria-label={`Open ${connection.displayName} integration actions`}
|
|
156
|
+
/>
|
|
157
|
+
</DropdownMenuTrigger>
|
|
158
|
+
<DropdownMenuContent align="end">
|
|
159
|
+
{connection.externalUrl ? (
|
|
160
|
+
<DropdownMenuItem asChild>
|
|
161
|
+
<a href={connection.externalUrl} target="_blank" rel="noreferrer noopener">
|
|
162
|
+
{providerName ? `Open in ${providerName}` : 'Open provider settings'}
|
|
163
|
+
</a>
|
|
164
|
+
</DropdownMenuItem>
|
|
165
|
+
) : null}
|
|
166
|
+
<DropdownMenuItem onSelect={() => onUse(connection.id)}>
|
|
167
|
+
Use this integration
|
|
168
|
+
</DropdownMenuItem>
|
|
169
|
+
{workspaceSlug ? (
|
|
170
|
+
<DropdownMenuItem asChild>
|
|
171
|
+
<Link
|
|
172
|
+
to="/w/$workspaceSlug/settings/events"
|
|
173
|
+
params={{workspaceSlug}}
|
|
174
|
+
search={{source: [connection.slug], event: [recentEventsEvent]}}
|
|
175
|
+
>
|
|
176
|
+
View recent events
|
|
177
|
+
</Link>
|
|
178
|
+
</DropdownMenuItem>
|
|
179
|
+
) : null}
|
|
180
|
+
<DropdownMenuSeparator />
|
|
181
|
+
<DropdownMenuItem disabled={isMutating} onSelect={() => onSetActive(!active)}>
|
|
182
|
+
{active ? 'Disable integration' : 'Enable integration'}
|
|
162
183
|
</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>
|
|
184
|
+
<DropdownMenuItem disabled={isMutating} onSelect={() => onDelete(connection.id)}>
|
|
185
|
+
Delete integration
|
|
176
186
|
</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>
|
|
187
|
+
</DropdownMenuContent>
|
|
188
|
+
</DropdownMenu>
|
|
189
|
+
</li>
|
|
190
|
+
</PanelRow>
|
|
188
191
|
);
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
function InstalledSkeleton({label}: {label: string}) {
|
|
192
195
|
return (
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
196
|
+
<Panel>
|
|
197
|
+
<PanelBody asChild>
|
|
198
|
+
<ul role="status" aria-label={label}>
|
|
199
|
+
{[0, 1, 2].map((row) => (
|
|
200
|
+
<PanelRow
|
|
201
|
+
asChild
|
|
202
|
+
className="justify-start gap-cluster hover:bg-background-neutral-base"
|
|
203
|
+
key={row}
|
|
204
|
+
>
|
|
205
|
+
<li>
|
|
206
|
+
<Skeleton className="size-24 shrink-0" />
|
|
207
|
+
<div className="flex min-w-0 flex-1 flex-col gap-tight">
|
|
208
|
+
<Skeleton className="h-16 w-120" />
|
|
209
|
+
<Skeleton className="h-12 w-80" />
|
|
210
|
+
</div>
|
|
211
|
+
<Skeleton className="h-20 w-72 shrink-0" />
|
|
212
|
+
</li>
|
|
213
|
+
</PanelRow>
|
|
214
|
+
))}
|
|
215
|
+
</ul>
|
|
216
|
+
</PanelBody>
|
|
217
|
+
</Panel>
|
|
209
218
|
);
|
|
210
219
|
}
|
|
@@ -148,9 +148,6 @@ export function IntegrationGalleryForWorkspace({
|
|
|
148
148
|
<section className="flex flex-col gap-group" aria-label="Available integrations">
|
|
149
149
|
<div className="flex flex-col gap-tight">
|
|
150
150
|
<Header variant="h3">Available integrations</Header>
|
|
151
|
-
<Text size="sm" className="text-foreground-neutral-muted">
|
|
152
|
-
Providers available to install in this workspace.
|
|
153
|
-
</Text>
|
|
154
151
|
</div>
|
|
155
152
|
|
|
156
153
|
{workspaceSlug ? (
|
|
@@ -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
|
},
|
|
@@ -244,7 +244,9 @@ describe('IntegrationGallery — installed section', () => {
|
|
|
244
244
|
|
|
245
245
|
expect(await screen.findByText('acme-one')).toBeVisible();
|
|
246
246
|
expect(screen.getByText('acme-two')).toBeVisible();
|
|
247
|
-
expect(
|
|
247
|
+
expect(
|
|
248
|
+
screen.queryByText('Provider accounts installed in this workspace.'),
|
|
249
|
+
).not.toBeInTheDocument();
|
|
248
250
|
});
|
|
249
251
|
|
|
250
252
|
test('sorts stably by provider name then created_at regardless of input order', async () => {
|
|
@@ -592,7 +594,9 @@ describe('IntegrationGallery: available section', () => {
|
|
|
592
594
|
expect(await screen.findByRole('link', {name: 'Install GitHub'})).toBeVisible();
|
|
593
595
|
expect(screen.getByRole('link', {name: 'Install Sentry'})).toBeVisible();
|
|
594
596
|
expect(screen.getByRole('button', {name: 'Add Webhook'})).toBeVisible();
|
|
595
|
-
expect(
|
|
597
|
+
expect(
|
|
598
|
+
screen.queryByText('Providers available to install in this workspace.'),
|
|
599
|
+
).not.toBeInTheDocument();
|
|
596
600
|
});
|
|
597
601
|
|
|
598
602
|
test('exposes each available tile as a single link with no nested button', async () => {
|
|
@@ -600,8 +604,9 @@ describe('IntegrationGallery: available section', () => {
|
|
|
600
604
|
|
|
601
605
|
const link = await screen.findByRole('link', {name: 'Install GitHub'});
|
|
602
606
|
|
|
603
|
-
|
|
604
|
-
|
|
607
|
+
// Cells run edge to edge inside the panel, so the ring is inset rather than
|
|
608
|
+
// the outset button ring the panel's `overflow-hidden` would crop.
|
|
609
|
+
expect(link).toHaveClass('focus-visible:shadow-focus-inset');
|
|
605
610
|
expect(link.className).not.toContain('shadow-button-secondary');
|
|
606
611
|
expect(within(link).getByText('Install')).toBeVisible();
|
|
607
612
|
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
|
|