@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,49 @@
|
|
|
1
|
+
import type {IntegrationCapabilityDto} from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import {useActiveWorkspace} from '@shipfox/client-auth';
|
|
3
|
+
import {IntegrationGalleryForWorkspace} from './integration-gallery-for-workspace.js';
|
|
4
|
+
|
|
5
|
+
export interface IntegrationGalleryProps {
|
|
6
|
+
capability?: IntegrationCapabilityDto;
|
|
7
|
+
emptyProvidersMessage?: string;
|
|
8
|
+
workspaceId?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function IntegrationGallery({
|
|
12
|
+
capability,
|
|
13
|
+
emptyProvidersMessage = 'Enable at least one provider in the application settings.',
|
|
14
|
+
workspaceId,
|
|
15
|
+
}: IntegrationGalleryProps) {
|
|
16
|
+
if (workspaceId) {
|
|
17
|
+
return (
|
|
18
|
+
<IntegrationGalleryForWorkspace
|
|
19
|
+
workspaceId={workspaceId}
|
|
20
|
+
capability={capability}
|
|
21
|
+
emptyProvidersMessage={emptyProvidersMessage}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<RoutedIntegrationGallery
|
|
28
|
+
capability={capability}
|
|
29
|
+
emptyProvidersMessage={emptyProvidersMessage}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function RoutedIntegrationGallery({
|
|
35
|
+
capability,
|
|
36
|
+
emptyProvidersMessage,
|
|
37
|
+
}: {
|
|
38
|
+
capability: IntegrationCapabilityDto | undefined;
|
|
39
|
+
emptyProvidersMessage: string;
|
|
40
|
+
}) {
|
|
41
|
+
const workspace = useActiveWorkspace();
|
|
42
|
+
return (
|
|
43
|
+
<IntegrationGalleryForWorkspace
|
|
44
|
+
workspaceId={workspace.id}
|
|
45
|
+
capability={capability}
|
|
46
|
+
emptyProvidersMessage={emptyProvidersMessage}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import {linearWebhookEventNames} from '@shipfox/api-integration-linear-dto';
|
|
3
|
+
import {WEBHOOK_RECEIVED_EVENT} from '@shipfox/api-integration-webhook-dto';
|
|
4
|
+
import {usageEventsForConnection} from './integration-usage-events.js';
|
|
5
|
+
|
|
6
|
+
const baseConnection = {
|
|
7
|
+
id: '44444444-4444-4444-8444-444444444444',
|
|
8
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
9
|
+
provider: 'github',
|
|
10
|
+
external_account_id: 'installation-1',
|
|
11
|
+
slug: 'github_acme_corp',
|
|
12
|
+
display_name: 'acme-corp',
|
|
13
|
+
lifecycle_status: 'active',
|
|
14
|
+
capabilities: [],
|
|
15
|
+
created_at: '2026-03-12T00:00:00.000Z',
|
|
16
|
+
updated_at: '2026-03-12T00:00:00.000Z',
|
|
17
|
+
} satisfies IntegrationConnectionDto;
|
|
18
|
+
|
|
19
|
+
describe('usageEventsForConnection', () => {
|
|
20
|
+
it('lists the GitHub webhook events used by workflow triggers', () => {
|
|
21
|
+
const connection = {
|
|
22
|
+
...baseConnection,
|
|
23
|
+
provider: 'github',
|
|
24
|
+
capabilities: ['source_control'],
|
|
25
|
+
} satisfies IntegrationConnectionDto;
|
|
26
|
+
|
|
27
|
+
const events = usageEventsForConnection(connection);
|
|
28
|
+
const values = events.map((event) => event.value);
|
|
29
|
+
|
|
30
|
+
expect(values[0]).toBe('push');
|
|
31
|
+
expect(values).toEqual(expect.arrayContaining(['pull_request', 'workflow_run']));
|
|
32
|
+
expect(events.find((event) => event.value === 'workflow_run')?.label).toBe('workflow_run');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('uses the webhook received event for webhook connections', () => {
|
|
36
|
+
const connection = {
|
|
37
|
+
...baseConnection,
|
|
38
|
+
provider: 'webhook',
|
|
39
|
+
slug: 'stripe-prod',
|
|
40
|
+
} satisfies IntegrationConnectionDto;
|
|
41
|
+
|
|
42
|
+
const events = usageEventsForConnection(connection);
|
|
43
|
+
|
|
44
|
+
expect(events).toEqual([{value: WEBHOOK_RECEIVED_EVENT, label: WEBHOOK_RECEIVED_EVENT}]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('uses source-control push for uncataloged source-control providers', () => {
|
|
48
|
+
const connection = {
|
|
49
|
+
...baseConnection,
|
|
50
|
+
provider: 'gitea',
|
|
51
|
+
capabilities: ['source_control'],
|
|
52
|
+
} satisfies IntegrationConnectionDto;
|
|
53
|
+
|
|
54
|
+
const events = usageEventsForConnection(connection);
|
|
55
|
+
|
|
56
|
+
expect(events).toEqual([{value: 'push', label: 'push'}]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('uses Linear webhook names directly', () => {
|
|
60
|
+
const connection = {
|
|
61
|
+
...baseConnection,
|
|
62
|
+
provider: 'linear',
|
|
63
|
+
} satisfies IntegrationConnectionDto;
|
|
64
|
+
|
|
65
|
+
const events = usageEventsForConnection(connection);
|
|
66
|
+
|
|
67
|
+
expect(events.map((event) => event.value)).toEqual(linearWebhookEventNames);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('falls back to a generic received event for uncataloged providers', () => {
|
|
71
|
+
const connection = {
|
|
72
|
+
...baseConnection,
|
|
73
|
+
provider: 'mystery',
|
|
74
|
+
} satisfies IntegrationConnectionDto;
|
|
75
|
+
|
|
76
|
+
const events = usageEventsForConnection(connection);
|
|
77
|
+
|
|
78
|
+
expect(events).toEqual([{value: 'received', label: 'received'}]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type {IntegrationConnectionDto, SentryIssueAction} from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import {SENTRY_ISSUE_ACTIONS} from '@shipfox/api-integration-core-dto';
|
|
3
|
+
import {linearWebhookEventNames} from '@shipfox/api-integration-linear-dto';
|
|
4
|
+
import {WEBHOOK_RECEIVED_EVENT} from '@shipfox/api-integration-webhook-dto';
|
|
5
|
+
import type {IntegrationUsageEvent} from './integration-usage-modal.js';
|
|
6
|
+
|
|
7
|
+
const GITHUB_EVENTS = [
|
|
8
|
+
'push',
|
|
9
|
+
'branch_protection_configuration',
|
|
10
|
+
'branch_protection_rule',
|
|
11
|
+
'check_run',
|
|
12
|
+
'check_suite',
|
|
13
|
+
'code_scanning_alert',
|
|
14
|
+
'commit_comment',
|
|
15
|
+
'create',
|
|
16
|
+
'custom_property',
|
|
17
|
+
'custom_property_values',
|
|
18
|
+
'delete',
|
|
19
|
+
'dependabot_alert',
|
|
20
|
+
'deploy_key',
|
|
21
|
+
'deployment',
|
|
22
|
+
'deployment_protection_rule',
|
|
23
|
+
'deployment_review',
|
|
24
|
+
'deployment_status',
|
|
25
|
+
'discussion',
|
|
26
|
+
'discussion_comment',
|
|
27
|
+
'fork',
|
|
28
|
+
'github_app_authorization',
|
|
29
|
+
'gollum',
|
|
30
|
+
'installation',
|
|
31
|
+
'installation_repositories',
|
|
32
|
+
'installation_target',
|
|
33
|
+
'issue_comment',
|
|
34
|
+
'issue_dependencies',
|
|
35
|
+
'issues',
|
|
36
|
+
'label',
|
|
37
|
+
'marketplace_purchase',
|
|
38
|
+
'member',
|
|
39
|
+
'membership',
|
|
40
|
+
'merge_group',
|
|
41
|
+
'meta',
|
|
42
|
+
'milestone',
|
|
43
|
+
'org_block',
|
|
44
|
+
'organization',
|
|
45
|
+
'package',
|
|
46
|
+
'page_build',
|
|
47
|
+
'personal_access_token_request',
|
|
48
|
+
'ping',
|
|
49
|
+
'project',
|
|
50
|
+
'project_card',
|
|
51
|
+
'project_column',
|
|
52
|
+
'projects_v2',
|
|
53
|
+
'projects_v2_item',
|
|
54
|
+
'projects_v2_status_update',
|
|
55
|
+
'public',
|
|
56
|
+
'pull_request',
|
|
57
|
+
'pull_request_review',
|
|
58
|
+
'pull_request_review_comment',
|
|
59
|
+
'pull_request_review_thread',
|
|
60
|
+
'registry_package',
|
|
61
|
+
'release',
|
|
62
|
+
'repository',
|
|
63
|
+
'repository_advisory',
|
|
64
|
+
'repository_dispatch',
|
|
65
|
+
'repository_import',
|
|
66
|
+
'repository_ruleset',
|
|
67
|
+
'repository_vulnerability_alert',
|
|
68
|
+
'secret_scanning_alert',
|
|
69
|
+
'secret_scanning_alert_location',
|
|
70
|
+
'secret_scanning_scan',
|
|
71
|
+
'security_advisory',
|
|
72
|
+
'security_and_analysis',
|
|
73
|
+
'sponsorship',
|
|
74
|
+
'star',
|
|
75
|
+
'status',
|
|
76
|
+
'sub_issues',
|
|
77
|
+
'team',
|
|
78
|
+
'team_add',
|
|
79
|
+
'watch',
|
|
80
|
+
'workflow_dispatch',
|
|
81
|
+
'workflow_job',
|
|
82
|
+
'workflow_run',
|
|
83
|
+
] as const;
|
|
84
|
+
|
|
85
|
+
export function usageEventsForConnection(
|
|
86
|
+
connection: IntegrationConnectionDto,
|
|
87
|
+
): IntegrationUsageEvent[] {
|
|
88
|
+
if (connection.provider === 'webhook') {
|
|
89
|
+
return [{value: WEBHOOK_RECEIVED_EVENT, label: WEBHOOK_RECEIVED_EVENT}];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (connection.provider === 'github') {
|
|
93
|
+
return GITHUB_EVENTS.map((event) => ({value: event, label: event}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (connection.provider === 'sentry') {
|
|
97
|
+
return SENTRY_ISSUE_ACTIONS.map((action) => ({
|
|
98
|
+
value: `issue.${action}`,
|
|
99
|
+
label: sentryIssueEventLabel(action),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (connection.provider === 'linear') {
|
|
104
|
+
return linearWebhookEventNames.map((event) => ({value: event, label: event}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (connection.capabilities.includes('source_control')) {
|
|
108
|
+
return [{value: 'push', label: 'push'}];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [{value: 'received', label: 'received'}];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sentryIssueEventLabel(action: SentryIssueAction): string {
|
|
115
|
+
return `issue.${action}`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import {Button} from '@shipfox/react-ui/button';
|
|
3
|
+
import {
|
|
4
|
+
CodeBlock,
|
|
5
|
+
CodeBlockBody,
|
|
6
|
+
CodeBlockContent,
|
|
7
|
+
CodeBlockCopyButton,
|
|
8
|
+
CodeBlockFilename,
|
|
9
|
+
CodeBlockFiles,
|
|
10
|
+
CodeBlockHeader,
|
|
11
|
+
CodeBlockItem,
|
|
12
|
+
} from '@shipfox/react-ui/code-block';
|
|
13
|
+
import {
|
|
14
|
+
Modal,
|
|
15
|
+
ModalBody,
|
|
16
|
+
ModalContent,
|
|
17
|
+
ModalFooter,
|
|
18
|
+
ModalHeader,
|
|
19
|
+
ModalTitle,
|
|
20
|
+
} from '@shipfox/react-ui/modal';
|
|
21
|
+
import {
|
|
22
|
+
Select,
|
|
23
|
+
SelectContent,
|
|
24
|
+
SelectItem,
|
|
25
|
+
SelectTrigger,
|
|
26
|
+
SelectValue,
|
|
27
|
+
} from '@shipfox/react-ui/select';
|
|
28
|
+
import {Text} from '@shipfox/react-ui/typography';
|
|
29
|
+
import type {ReactNode} from 'react';
|
|
30
|
+
import {useEffect, useMemo, useState} from 'react';
|
|
31
|
+
import {ConnectionStatusBadge} from '#connection-status-badge.js';
|
|
32
|
+
|
|
33
|
+
export interface IntegrationUsageEvent {
|
|
34
|
+
value: string;
|
|
35
|
+
label: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface IntegrationUsageModalProps {
|
|
39
|
+
connection: IntegrationConnectionDto | null;
|
|
40
|
+
events: IntegrationUsageEvent[];
|
|
41
|
+
open: boolean;
|
|
42
|
+
onOpenChange: (open: boolean) => void;
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const WORKFLOW_FILENAME = '.shipfox/workflows/integration.yml';
|
|
47
|
+
|
|
48
|
+
export function IntegrationUsageModal({
|
|
49
|
+
connection,
|
|
50
|
+
events,
|
|
51
|
+
open,
|
|
52
|
+
onOpenChange,
|
|
53
|
+
children,
|
|
54
|
+
}: IntegrationUsageModalProps) {
|
|
55
|
+
const [selectedEvent, setSelectedEvent] = useState('');
|
|
56
|
+
const eventValuesKey = events.map((event) => event.value).join('\u0000');
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const eventValues = eventValuesKey ? eventValuesKey.split('\u0000') : [];
|
|
60
|
+
setSelectedEvent((current) =>
|
|
61
|
+
eventValues.includes(current) ? current : (eventValues[0] ?? ''),
|
|
62
|
+
);
|
|
63
|
+
}, [eventValuesKey]);
|
|
64
|
+
|
|
65
|
+
const workflowExample = connection
|
|
66
|
+
? buildWorkflowExample({source: connection.slug, event: selectedEvent})
|
|
67
|
+
: '';
|
|
68
|
+
const data = useMemo(
|
|
69
|
+
() => [{language: 'yaml', filename: WORKFLOW_FILENAME, code: workflowExample}],
|
|
70
|
+
[workflowExample],
|
|
71
|
+
);
|
|
72
|
+
const title = connection ? `Use ${connection.display_name}` : 'Use integration';
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Modal open={open && connection !== null} onOpenChange={onOpenChange}>
|
|
76
|
+
<ModalContent aria-describedby={undefined} className="max-h-[calc(100vh-32px)] max-w-[640px]">
|
|
77
|
+
<ModalTitle className="sr-only">{title}</ModalTitle>
|
|
78
|
+
<ModalHeader>
|
|
79
|
+
<div className="flex min-w-0 flex-col gap-2">
|
|
80
|
+
<div className="flex min-w-0 items-center gap-8">
|
|
81
|
+
<Text size="lg" aria-hidden="true" className="truncate">
|
|
82
|
+
{title}
|
|
83
|
+
</Text>
|
|
84
|
+
{connection ? (
|
|
85
|
+
<ConnectionStatusBadge status={connection.lifecycle_status} className="shrink-0" />
|
|
86
|
+
) : null}
|
|
87
|
+
</div>
|
|
88
|
+
<Text size="sm" className="text-foreground-neutral-muted">
|
|
89
|
+
Reference this connection from a workflow trigger.
|
|
90
|
+
</Text>
|
|
91
|
+
</div>
|
|
92
|
+
</ModalHeader>
|
|
93
|
+
{connection ? (
|
|
94
|
+
<>
|
|
95
|
+
<ModalBody className="min-h-0 flex-1 gap-24 overflow-y-auto overflow-x-clip scrollbar">
|
|
96
|
+
<section className="flex w-full flex-col gap-12">
|
|
97
|
+
<Text size="md" bold>
|
|
98
|
+
Usage
|
|
99
|
+
</Text>
|
|
100
|
+
{events.length > 1 ? (
|
|
101
|
+
<div className="flex flex-col gap-6">
|
|
102
|
+
<label
|
|
103
|
+
htmlFor="integration-usage-event"
|
|
104
|
+
className="font-display text-sm font-medium text-foreground-neutral-base"
|
|
105
|
+
>
|
|
106
|
+
Event
|
|
107
|
+
</label>
|
|
108
|
+
<Select value={selectedEvent} onValueChange={setSelectedEvent}>
|
|
109
|
+
<SelectTrigger id="integration-usage-event">
|
|
110
|
+
<SelectValue placeholder="Select event" />
|
|
111
|
+
</SelectTrigger>
|
|
112
|
+
<SelectContent>
|
|
113
|
+
{events.map((event) => (
|
|
114
|
+
<SelectItem key={event.value} value={event.value}>
|
|
115
|
+
{event.label}
|
|
116
|
+
</SelectItem>
|
|
117
|
+
))}
|
|
118
|
+
</SelectContent>
|
|
119
|
+
</Select>
|
|
120
|
+
</div>
|
|
121
|
+
) : null}
|
|
122
|
+
<CodeBlock data={data} className="h-auto min-h-0 rounded-8">
|
|
123
|
+
<CodeBlockHeader>
|
|
124
|
+
<CodeBlockFiles>
|
|
125
|
+
{(item) => (
|
|
126
|
+
<CodeBlockFilename value={item.filename}>{item.filename}</CodeBlockFilename>
|
|
127
|
+
)}
|
|
128
|
+
</CodeBlockFiles>
|
|
129
|
+
<CodeBlockCopyButton />
|
|
130
|
+
</CodeBlockHeader>
|
|
131
|
+
<CodeBlockBody>
|
|
132
|
+
{(item) => (
|
|
133
|
+
<CodeBlockItem value={item.filename}>
|
|
134
|
+
<CodeBlockContent
|
|
135
|
+
language="yaml"
|
|
136
|
+
syntaxHighlighting
|
|
137
|
+
highlightedLineRange={{startLine: 3, endLine: 4}}
|
|
138
|
+
>
|
|
139
|
+
{item.code}
|
|
140
|
+
</CodeBlockContent>
|
|
141
|
+
</CodeBlockItem>
|
|
142
|
+
)}
|
|
143
|
+
</CodeBlockBody>
|
|
144
|
+
</CodeBlock>
|
|
145
|
+
</section>
|
|
146
|
+
{children ? (
|
|
147
|
+
<section className="flex w-full flex-col gap-12">
|
|
148
|
+
<Text size="md" bold>
|
|
149
|
+
Details
|
|
150
|
+
</Text>
|
|
151
|
+
{children}
|
|
152
|
+
</section>
|
|
153
|
+
) : null}
|
|
154
|
+
</ModalBody>
|
|
155
|
+
<ModalFooter>
|
|
156
|
+
<Button type="button" onClick={() => onOpenChange(false)}>
|
|
157
|
+
Done
|
|
158
|
+
</Button>
|
|
159
|
+
</ModalFooter>
|
|
160
|
+
</>
|
|
161
|
+
) : null}
|
|
162
|
+
</ModalContent>
|
|
163
|
+
</Modal>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildWorkflowExample({source, event}: {source: string; event: string}) {
|
|
168
|
+
return [
|
|
169
|
+
'triggers:',
|
|
170
|
+
' integration:',
|
|
171
|
+
` source: ${source}`,
|
|
172
|
+
` event: ${event}`,
|
|
173
|
+
'jobs:',
|
|
174
|
+
' handle-event:',
|
|
175
|
+
' steps:',
|
|
176
|
+
' - run: echo "Received event"',
|
|
177
|
+
].join('\n');
|
|
178
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IntegrationProviderDto,
|
|
3
|
+
ListIntegrationProvidersResponseDto,
|
|
4
|
+
} from '@shipfox/api-integration-core-dto';
|
|
5
|
+
import {QueryLoadError} from '@shipfox/client-ui';
|
|
6
|
+
import {Card} from '@shipfox/react-ui/card';
|
|
7
|
+
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
8
|
+
import {Icon} from '@shipfox/react-ui/icon';
|
|
9
|
+
import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
10
|
+
import {Text} from '@shipfox/react-ui/typography';
|
|
11
|
+
import {cn} from '@shipfox/react-ui/utils';
|
|
12
|
+
import type {UseQueryResult} from '@tanstack/react-query';
|
|
13
|
+
import {Link} from '@tanstack/react-router';
|
|
14
|
+
import {IntegrationIcon} from '#integration-icon.js';
|
|
15
|
+
import {PROVIDER_CATALOG} from '#provider-catalog.js';
|
|
16
|
+
|
|
17
|
+
export interface ProviderGridProps {
|
|
18
|
+
providersQuery: UseQueryResult<ListIntegrationProvidersResponseDto, Error>;
|
|
19
|
+
workspaceId: string;
|
|
20
|
+
emptyMessage: string;
|
|
21
|
+
loadingLabel?: string;
|
|
22
|
+
errorSubject?: string;
|
|
23
|
+
onOpenProvider?: ((provider: string) => void) | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const PROVIDER_GRID_CLASS = 'grid grid-cols-2 gap-12 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
|
+
export function ProviderGrid({
|
|
32
|
+
providersQuery,
|
|
33
|
+
workspaceId,
|
|
34
|
+
emptyMessage,
|
|
35
|
+
loadingLabel = 'Loading providers',
|
|
36
|
+
errorSubject = 'available integrations',
|
|
37
|
+
onOpenProvider,
|
|
38
|
+
}: ProviderGridProps) {
|
|
39
|
+
const providers = providersQuery.data?.providers ?? [];
|
|
40
|
+
const installableProviders = providers.filter((provider) => PROVIDER_CATALOG[provider.provider]);
|
|
41
|
+
|
|
42
|
+
if (providersQuery.isPending) return <ProviderGridSkeleton label={loadingLabel} />;
|
|
43
|
+
|
|
44
|
+
if (providersQuery.isError && providersQuery.data === undefined) {
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn(PROVIDER_SURFACE_CLASS, 'px-16')}>
|
|
47
|
+
<QueryLoadError query={providersQuery} subject={errorSubject} />
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (providersQuery.data !== undefined && installableProviders.length === 0) {
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn(PROVIDER_SURFACE_CLASS, 'px-16')}>
|
|
55
|
+
<EmptyState
|
|
56
|
+
icon="componentLine"
|
|
57
|
+
title="No integrations available"
|
|
58
|
+
description={emptyMessage}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<ul className={PROVIDER_GRID_CLASS}>
|
|
66
|
+
{installableProviders.map((provider) => (
|
|
67
|
+
<li key={provider.provider}>
|
|
68
|
+
<ProviderCard
|
|
69
|
+
provider={provider}
|
|
70
|
+
workspaceId={workspaceId}
|
|
71
|
+
onOpenProvider={onOpenProvider}
|
|
72
|
+
/>
|
|
73
|
+
</li>
|
|
74
|
+
))}
|
|
75
|
+
</ul>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ProviderCard({
|
|
80
|
+
provider,
|
|
81
|
+
workspaceId,
|
|
82
|
+
onOpenProvider,
|
|
83
|
+
}: {
|
|
84
|
+
provider: IntegrationProviderDto;
|
|
85
|
+
workspaceId: string;
|
|
86
|
+
onOpenProvider?: ((provider: string) => void) | undefined;
|
|
87
|
+
}) {
|
|
88
|
+
const catalog = PROVIDER_CATALOG[provider.provider];
|
|
89
|
+
if (!catalog) return null;
|
|
90
|
+
|
|
91
|
+
if (catalog.kind === 'modal-connect') {
|
|
92
|
+
return (
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
aria-label={`Add ${provider.display_name}`}
|
|
96
|
+
onClick={() => onOpenProvider?.(provider.provider)}
|
|
97
|
+
className="group flex h-full w-full min-w-0 items-center justify-between gap-12 rounded-8 border border-border-neutral-base bg-background-neutral-base p-16 text-left transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
|
|
98
|
+
>
|
|
99
|
+
<ProviderCardContent provider={provider} action="Add" />
|
|
100
|
+
</button>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Link
|
|
106
|
+
to={catalog.setupPath}
|
|
107
|
+
params={{wid: workspaceId}}
|
|
108
|
+
aria-label={`Install ${provider.display_name}`}
|
|
109
|
+
className="group flex h-full min-w-0 items-center justify-between gap-12 rounded-8 border border-border-neutral-base bg-background-neutral-base p-16 transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
|
|
110
|
+
>
|
|
111
|
+
<ProviderCardContent provider={provider} action="Install" />
|
|
112
|
+
</Link>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ProviderCardContent({
|
|
117
|
+
provider,
|
|
118
|
+
action,
|
|
119
|
+
}: {
|
|
120
|
+
provider: IntegrationProviderDto;
|
|
121
|
+
action: 'Add' | 'Install';
|
|
122
|
+
}) {
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<span className="flex min-w-0 items-center gap-12">
|
|
126
|
+
<IntegrationIcon
|
|
127
|
+
source={provider.provider}
|
|
128
|
+
aria-hidden
|
|
129
|
+
className="size-24 shrink-0 text-foreground-neutral-base"
|
|
130
|
+
/>
|
|
131
|
+
<Text as="span" size="md" bold className="truncate">
|
|
132
|
+
{provider.display_name}
|
|
133
|
+
</Text>
|
|
134
|
+
</span>
|
|
135
|
+
<span className="flex shrink-0 items-center gap-4 text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
|
|
136
|
+
<Text as="span" size="sm">
|
|
137
|
+
{action}
|
|
138
|
+
</Text>
|
|
139
|
+
<Icon name="chevronRight" className="size-16" />
|
|
140
|
+
</span>
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function ProviderGridSkeleton({label}: {label: string}) {
|
|
146
|
+
return (
|
|
147
|
+
<ul role="status" aria-label={label} className={PROVIDER_GRID_CLASS}>
|
|
148
|
+
{[0, 1, 2, 3].map((tile) => (
|
|
149
|
+
<li key={tile}>
|
|
150
|
+
<Card className="h-full p-16">
|
|
151
|
+
<div className="flex items-center justify-between gap-12">
|
|
152
|
+
<div className="flex min-w-0 items-center gap-12">
|
|
153
|
+
<Skeleton className="size-24 shrink-0" />
|
|
154
|
+
<Skeleton className="h-16 w-100" />
|
|
155
|
+
</div>
|
|
156
|
+
<Skeleton className="h-16 w-64 shrink-0" />
|
|
157
|
+
</div>
|
|
158
|
+
</Card>
|
|
159
|
+
</li>
|
|
160
|
+
))}
|
|
161
|
+
</ul>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import '@testing-library/jest-dom/vitest';
|
|
3
|
+
import {ApiError} from '@shipfox/client-api';
|
|
4
|
+
import {screen, waitFor} from '@testing-library/react';
|
|
5
|
+
import {StrictMode} from 'react';
|
|
6
|
+
import {INTEGRATIONS_TEST_WID, renderIntegrationsPage} from '#test/render.js';
|
|
7
|
+
import {RedirectInstallPage} from './redirect-install-page.js';
|
|
8
|
+
|
|
9
|
+
function renderInstallPage(
|
|
10
|
+
props: Parameters<typeof RedirectInstallPage>[0],
|
|
11
|
+
options?: {strict?: boolean},
|
|
12
|
+
) {
|
|
13
|
+
return renderIntegrationsPage({
|
|
14
|
+
path: `/workspaces/${INTEGRATIONS_TEST_WID}/integrations/github`,
|
|
15
|
+
routePath: '/workspaces/$wid/integrations/github',
|
|
16
|
+
element: options?.strict ? (
|
|
17
|
+
<StrictMode>
|
|
18
|
+
<RedirectInstallPage {...props} />
|
|
19
|
+
</StrictMode>
|
|
20
|
+
) : (
|
|
21
|
+
<RedirectInstallPage {...props} />
|
|
22
|
+
),
|
|
23
|
+
extraRoutes: ['/workspaces/$wid/integrations'],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('RedirectInstallPage', () => {
|
|
28
|
+
test('requests the install URL and leaves the app', async () => {
|
|
29
|
+
const installRequest = vi
|
|
30
|
+
.fn()
|
|
31
|
+
.mockResolvedValue({install_url: 'https://provider.test/install'});
|
|
32
|
+
const assignLocation = vi.fn();
|
|
33
|
+
const beforeRedirect = vi.fn();
|
|
34
|
+
|
|
35
|
+
renderInstallPage({
|
|
36
|
+
installRequest,
|
|
37
|
+
errorFallbackMessage: 'Could not start install.',
|
|
38
|
+
beforeRedirect,
|
|
39
|
+
assignLocation,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await waitFor(() =>
|
|
43
|
+
expect(assignLocation).toHaveBeenCalledWith('https://provider.test/install'),
|
|
44
|
+
);
|
|
45
|
+
expect(installRequest).toHaveBeenCalledWith({workspace_id: INTEGRATIONS_TEST_WID});
|
|
46
|
+
expect(beforeRedirect).toHaveBeenCalledWith(INTEGRATIONS_TEST_WID);
|
|
47
|
+
expect(beforeRedirect.mock.invocationCallOrder[0]).toBeLessThan(
|
|
48
|
+
installRequest.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('shows the API error message with a back link on failure', async () => {
|
|
53
|
+
const installRequest = vi
|
|
54
|
+
.fn()
|
|
55
|
+
.mockRejectedValue(
|
|
56
|
+
new ApiError({message: 'Sentry app not configured', code: 'bad-config', status: 422}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
renderInstallPage({
|
|
60
|
+
installRequest,
|
|
61
|
+
errorFallbackMessage: 'Could not start install.',
|
|
62
|
+
assignLocation: vi.fn(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Alert mounts via framer-motion (opacity 0 in jsdom), so assert presence.
|
|
66
|
+
expect(await screen.findByText('Sentry app not configured')).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByRole('link', {name: 'Back to integrations'})).toBeVisible();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('requests the install URL exactly once in Strict Mode', async () => {
|
|
71
|
+
const installRequest = vi
|
|
72
|
+
.fn()
|
|
73
|
+
.mockResolvedValue({install_url: 'https://provider.test/install'});
|
|
74
|
+
|
|
75
|
+
renderInstallPage(
|
|
76
|
+
{
|
|
77
|
+
installRequest,
|
|
78
|
+
errorFallbackMessage: 'Could not start install.',
|
|
79
|
+
assignLocation: vi.fn(),
|
|
80
|
+
},
|
|
81
|
+
{strict: true},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await waitFor(() => expect(installRequest).toHaveBeenCalledTimes(1));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('falls back to the provided message for unknown errors', async () => {
|
|
88
|
+
const installRequest = vi.fn().mockRejectedValue(new Error('network down'));
|
|
89
|
+
|
|
90
|
+
renderInstallPage({
|
|
91
|
+
installRequest,
|
|
92
|
+
errorFallbackMessage: 'Could not start install.',
|
|
93
|
+
assignLocation: vi.fn(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(await screen.findByText('Could not start install.')).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
});
|