@stackone/hub 0.1.0 → 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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/dev/main.tsx +3 -1
- package/dev/vite-env.d.ts +1 -1
- package/package.json +12 -8
- package/src/Hub.tsx +50 -0
- package/src/StackOneHub.tsx +64 -25
- package/src/modules/integration-picker/IntegrationPicker.tsx +22 -14
- package/src/modules/integration-picker/components/IntegrationFields.tsx +73 -59
- package/src/modules/integration-picker/components/cardFooter.tsx +11 -11
- package/src/modules/integration-picker/components/cardTitle.tsx +8 -6
- package/src/modules/integration-picker/hooks/useIntegrationPicker.ts +201 -28
- package/src/modules/integration-picker/queries.ts +2 -2
- package/src/modules/integration-picker/types.ts +10 -0
- package/src/shared/components/error.tsx +2 -3
- package/src/shared/components/success.tsx +24 -17
- package/src/shared/contexts/featureFlagContext.tsx +26 -0
- package/src/shared/hooks/useFeatureFlags.ts +24 -0
- package/src/shared/queries.ts +12 -0
- package/src/shared/types/featureFlags.ts +1 -0
- package/.yalc/@stackone/malachite/README.md +0 -1
- package/.yalc/@stackone/malachite/package.json +0 -37
- package/.yalc/@stackone/malachite/yalc.sig +0 -1
- package/yalc.lock +0 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.2.0"}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/StackOneHQ/hub/compare/hub-v0.1.0...hub-v0.2.0) (2025-08-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add external trigger token rendering ([f1c0ea2](https://github.com/StackOneHQ/hub/commit/f1c0ea22496b4b0bd92a33d69e4af5492b96089c))
|
|
9
|
+
* expression and conditional rendering ([1762d3a](https://github.com/StackOneHQ/hub/commit/1762d3aad03fccc55a606bb5e154b85e8289f000))
|
|
10
|
+
* feature flag context and hook ([a9d00a9](https://github.com/StackOneHQ/hub/commit/a9d00a9e81f3276c82f5e98c26e0b5648a4ad0cd))
|
|
11
|
+
* render dropdowns and textareas with malachite ([9e59a0d](https://github.com/StackOneHQ/hub/commit/9e59a0d864af6c06cc7b653ff393291877895812))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* add border to dropdown ([2fb57e1](https://github.com/StackOneHQ/hub/commit/2fb57e139be16e646d3404bd5e13fd010cae8e67))
|
|
17
|
+
* add theme support again ([76a092b](https://github.com/StackOneHQ/hub/commit/76a092b3625ac850af9b3256569337cacd92da52))
|
|
18
|
+
* build ([1eb7894](https://github.com/StackOneHQ/hub/commit/1eb78945d7274349cb4572bac79ecd3e0929a57a))
|
|
19
|
+
* handle window close event ([617d999](https://github.com/StackOneHQ/hub/commit/617d9992c03496dab0b5e7bdba149b0f78314aee))
|
|
20
|
+
* input loop and card style ([a91e72a](https://github.com/StackOneHQ/hub/commit/a91e72a253a0da529712867e27192a397ed89ddf))
|
|
21
|
+
* key uniqueness ([c3d01c5](https://github.com/StackOneHQ/hub/commit/c3d01c513fd6d4b0e8526c6741b2ae2272ef2f0a))
|
|
22
|
+
|
|
3
23
|
## [0.1.0](https://github.com/StackOneHQ/hub/compare/hub-v0.0.1...hub-v0.1.0) (2025-07-11)
|
|
4
24
|
|
|
5
25
|
|
package/dev/main.tsx
CHANGED
|
@@ -11,8 +11,9 @@ const HubWrapper: React.FC = () => {
|
|
|
11
11
|
const [error, setError] = useState<string>();
|
|
12
12
|
const [token, setToken] = useState<string>();
|
|
13
13
|
const apiUrl = import.meta.env.VITE_API_URL ?? 'https://api.stackone.com';
|
|
14
|
+
const appUrl = import.meta.env.VITE_APP_URL ?? 'https://app.stackone.com';
|
|
14
15
|
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
|
15
|
-
const [accountId, setAccountId] = useState<string>(
|
|
16
|
+
const [accountId, setAccountId] = useState<string>();
|
|
16
17
|
|
|
17
18
|
const fetchToken = useCallback(async () => {
|
|
18
19
|
try {
|
|
@@ -81,6 +82,7 @@ const HubWrapper: React.FC = () => {
|
|
|
81
82
|
setMode(undefined);
|
|
82
83
|
}}
|
|
83
84
|
accountId={accountId}
|
|
85
|
+
appUrl={appUrl}
|
|
84
86
|
/>
|
|
85
87
|
</div>
|
|
86
88
|
);
|
package/dev/vite-env.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackone/hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "StackOne HUB",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -16,10 +16,14 @@
|
|
|
16
16
|
"author": "StackOne",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@stackone/
|
|
20
|
-
"@
|
|
21
|
-
"react": "^
|
|
22
|
-
|
|
19
|
+
"@stackone/expressions": "^0.16.0",
|
|
20
|
+
"@stackone/malachite": "^0.3.1",
|
|
21
|
+
"@tanstack/react-query": "^5.77.2"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": "18.3.1",
|
|
25
|
+
"react-dom": "18.3.1",
|
|
26
|
+
"react-hook-form": "7.60.0"
|
|
23
27
|
},
|
|
24
28
|
"devDependencies": {
|
|
25
29
|
"@biomejs/biome": "1.9.4",
|
|
@@ -29,8 +33,8 @@
|
|
|
29
33
|
"@rollup/plugin-terser": "^0.4.4",
|
|
30
34
|
"@rollup/plugin-typescript": "^12.1.2",
|
|
31
35
|
"@types/node": "^22.15.17",
|
|
32
|
-
"@types/react": "^18.
|
|
33
|
-
"@types/react-dom": "^18.
|
|
36
|
+
"@types/react": "^18.3.23",
|
|
37
|
+
"@types/react-dom": "^18.3.7",
|
|
34
38
|
"@vitejs/plugin-react-swc": "^3.9.0",
|
|
35
39
|
"react-to-webcomponent": "^2.0.1",
|
|
36
40
|
"rollup": "^4.40.2",
|
|
@@ -41,4 +45,4 @@
|
|
|
41
45
|
"tslib": "^2.8.1",
|
|
42
46
|
"typescript": "^5.8.3"
|
|
43
47
|
}
|
|
44
|
-
}
|
|
48
|
+
}
|
package/src/Hub.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { IntegrationPicker } from './modules/integration-picker/IntegrationPicker';
|
|
3
|
+
import { FeatureFlagProvider } from './shared/contexts/featureFlagContext';
|
|
4
|
+
import { getSettings } from './shared/queries';
|
|
5
|
+
import { HubModes } from './types/types';
|
|
6
|
+
|
|
7
|
+
interface HubProps {
|
|
8
|
+
mode: HubModes;
|
|
9
|
+
token: string;
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
dashboardUrl: string;
|
|
12
|
+
height: string;
|
|
13
|
+
onSuccess?: () => void;
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
onCancel?: () => void;
|
|
16
|
+
accountId?: string;
|
|
17
|
+
}
|
|
18
|
+
export const Hub = ({
|
|
19
|
+
mode,
|
|
20
|
+
token,
|
|
21
|
+
apiUrl,
|
|
22
|
+
dashboardUrl,
|
|
23
|
+
height,
|
|
24
|
+
onSuccess,
|
|
25
|
+
onClose,
|
|
26
|
+
onCancel,
|
|
27
|
+
accountId,
|
|
28
|
+
}: HubProps) => {
|
|
29
|
+
const { data: settings } = useQuery({
|
|
30
|
+
queryKey: ['settings'],
|
|
31
|
+
queryFn: () => getSettings(apiUrl, token),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<FeatureFlagProvider featureFlags={settings?.enabled_features ?? []}>
|
|
36
|
+
{mode === 'integration-picker' && (
|
|
37
|
+
<IntegrationPicker
|
|
38
|
+
token={token}
|
|
39
|
+
baseUrl={apiUrl}
|
|
40
|
+
dashboardUrl={dashboardUrl}
|
|
41
|
+
height={height}
|
|
42
|
+
onSuccess={onSuccess}
|
|
43
|
+
onClose={onClose}
|
|
44
|
+
onCancel={onCancel}
|
|
45
|
+
accountId={accountId}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
</FeatureFlagProvider>
|
|
49
|
+
);
|
|
50
|
+
};
|
package/src/StackOneHub.tsx
CHANGED
|
@@ -4,10 +4,16 @@ import {
|
|
|
4
4
|
FlexAlign,
|
|
5
5
|
FlexJustify,
|
|
6
6
|
FooterLinks,
|
|
7
|
-
|
|
7
|
+
MalachiteContext,
|
|
8
|
+
PartialMalachiteTheme,
|
|
8
9
|
Typography,
|
|
10
|
+
applyDarkTheme,
|
|
11
|
+
applyLightTheme,
|
|
12
|
+
applyTheme,
|
|
9
13
|
} from '@stackone/malachite';
|
|
10
14
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
15
|
+
import { useEffect } from 'react';
|
|
16
|
+
import { Hub } from './Hub';
|
|
11
17
|
import { CsvImporter } from './modules/csv-importer.tsx/CsvImporter';
|
|
12
18
|
import { IntegrationPicker } from './modules/integration-picker/IntegrationPicker';
|
|
13
19
|
import ErrorContainer from './shared/components/error';
|
|
@@ -18,8 +24,9 @@ interface StackOneHubProps {
|
|
|
18
24
|
mode?: HubModes;
|
|
19
25
|
token?: string;
|
|
20
26
|
baseUrl?: string;
|
|
27
|
+
appUrl?: string;
|
|
21
28
|
height?: string;
|
|
22
|
-
theme?: 'light' | 'dark';
|
|
29
|
+
theme?: 'light' | 'dark' | PartialMalachiteTheme;
|
|
23
30
|
accountId?: string;
|
|
24
31
|
onSuccess?: () => void;
|
|
25
32
|
onClose?: () => void;
|
|
@@ -30,6 +37,7 @@ export const StackOneHub: React.FC<StackOneHubProps> = ({
|
|
|
30
37
|
mode,
|
|
31
38
|
token,
|
|
32
39
|
baseUrl,
|
|
40
|
+
appUrl,
|
|
33
41
|
height = '500px',
|
|
34
42
|
theme = 'light',
|
|
35
43
|
accountId,
|
|
@@ -39,6 +47,17 @@ export const StackOneHub: React.FC<StackOneHubProps> = ({
|
|
|
39
47
|
}) => {
|
|
40
48
|
const defaultBaseUrl = 'https://api.stackone.com';
|
|
41
49
|
const apiUrl = baseUrl ?? defaultBaseUrl;
|
|
50
|
+
const defaultDashboardUrl = 'https://app.stackone.com';
|
|
51
|
+
const dashboardUrl = appUrl ?? defaultDashboardUrl;
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (theme === 'dark') {
|
|
54
|
+
applyDarkTheme();
|
|
55
|
+
} else if (theme === 'light') {
|
|
56
|
+
applyLightTheme();
|
|
57
|
+
} else {
|
|
58
|
+
applyTheme(theme);
|
|
59
|
+
}
|
|
60
|
+
}, [theme]);
|
|
42
61
|
|
|
43
62
|
const queryClient = new QueryClient({
|
|
44
63
|
defaultOptions: {
|
|
@@ -54,25 +73,45 @@ export const StackOneHub: React.FC<StackOneHubProps> = ({
|
|
|
54
73
|
|
|
55
74
|
if (!token) {
|
|
56
75
|
return (
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
<MalachiteContext>
|
|
77
|
+
<ErrorBoundary
|
|
78
|
+
fallback={
|
|
79
|
+
<Card height={height}>
|
|
80
|
+
<ErrorContainer />
|
|
81
|
+
</Card>
|
|
82
|
+
}
|
|
83
|
+
>
|
|
84
|
+
<Card height={height} footer={<FooterLinks />}>
|
|
85
|
+
<Flex justify={FlexJustify.Center} align={FlexAlign.Center} fullHeight>
|
|
86
|
+
<Typography.PageTitle>No token provided</Typography.PageTitle>
|
|
87
|
+
</Flex>
|
|
88
|
+
</Card>
|
|
89
|
+
</ErrorBoundary>
|
|
90
|
+
</MalachiteContext>
|
|
62
91
|
);
|
|
63
92
|
}
|
|
64
93
|
if (!mode) {
|
|
65
94
|
return (
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
<MalachiteContext>
|
|
96
|
+
<ErrorBoundary
|
|
97
|
+
fallback={
|
|
98
|
+
<Card height={height}>
|
|
99
|
+
<ErrorContainer />
|
|
100
|
+
</Card>
|
|
101
|
+
}
|
|
102
|
+
>
|
|
103
|
+
<Card height={height} footer={<FooterLinks />}>
|
|
104
|
+
<Flex justify={FlexJustify.Center} align={FlexAlign.Center} fullHeight>
|
|
105
|
+
<Typography.PageTitle>No mode selected</Typography.PageTitle>
|
|
106
|
+
</Flex>
|
|
107
|
+
</Card>
|
|
108
|
+
</ErrorBoundary>
|
|
109
|
+
</MalachiteContext>
|
|
71
110
|
);
|
|
72
111
|
}
|
|
73
112
|
|
|
74
113
|
return (
|
|
75
|
-
<
|
|
114
|
+
<MalachiteContext>
|
|
76
115
|
<ErrorBoundary
|
|
77
116
|
fallback={
|
|
78
117
|
<Card height={height}>
|
|
@@ -81,19 +120,19 @@ export const StackOneHub: React.FC<StackOneHubProps> = ({
|
|
|
81
120
|
}
|
|
82
121
|
>
|
|
83
122
|
<QueryClientProvider client={queryClient}>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
<Hub
|
|
124
|
+
mode={mode}
|
|
125
|
+
token={token}
|
|
126
|
+
apiUrl={apiUrl}
|
|
127
|
+
dashboardUrl={dashboardUrl}
|
|
128
|
+
height={height}
|
|
129
|
+
onSuccess={onSuccess}
|
|
130
|
+
accountId={accountId}
|
|
131
|
+
onClose={onClose}
|
|
132
|
+
onCancel={onCancel}
|
|
133
|
+
/>
|
|
95
134
|
</QueryClientProvider>
|
|
96
135
|
</ErrorBoundary>
|
|
97
|
-
</
|
|
136
|
+
</MalachiteContext>
|
|
98
137
|
);
|
|
99
138
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Card } from '@stackone/malachite';
|
|
2
|
+
import useFeatureFlags from '../../shared/hooks/useFeatureFlags';
|
|
2
3
|
import { IntegrationPickerContent } from './components/IntegrationPickerContent';
|
|
3
4
|
import CardFooter from './components/cardFooter';
|
|
4
5
|
import CardTitle from './components/cardTitle';
|
|
@@ -9,6 +10,7 @@ interface IntegrationPickerProps {
|
|
|
9
10
|
baseUrl: string;
|
|
10
11
|
height: string;
|
|
11
12
|
accountId?: string;
|
|
13
|
+
dashboardUrl?: string;
|
|
12
14
|
onSuccess?: () => void;
|
|
13
15
|
onClose?: () => void;
|
|
14
16
|
onCancel?: () => void;
|
|
@@ -20,7 +22,10 @@ export const IntegrationPicker: React.FC<IntegrationPickerProps> = ({
|
|
|
20
22
|
height,
|
|
21
23
|
accountId,
|
|
22
24
|
onSuccess,
|
|
25
|
+
dashboardUrl,
|
|
23
26
|
}) => {
|
|
27
|
+
const isHubLinkAccountReleaseEnabled = useFeatureFlags('hub_link_account_release');
|
|
28
|
+
|
|
24
29
|
const {
|
|
25
30
|
// Data
|
|
26
31
|
hubData,
|
|
@@ -48,6 +53,7 @@ export const IntegrationPicker: React.FC<IntegrationPickerProps> = ({
|
|
|
48
53
|
baseUrl,
|
|
49
54
|
accountId,
|
|
50
55
|
onSuccess,
|
|
56
|
+
dashboardUrl,
|
|
51
57
|
});
|
|
52
58
|
|
|
53
59
|
return (
|
|
@@ -70,20 +76,22 @@ export const IntegrationPicker: React.FC<IntegrationPickerProps> = ({
|
|
|
70
76
|
}
|
|
71
77
|
height={height}
|
|
72
78
|
>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
{isHubLinkAccountReleaseEnabled && (
|
|
80
|
+
<IntegrationPickerContent
|
|
81
|
+
isLoading={isLoading}
|
|
82
|
+
hasError={hasError}
|
|
83
|
+
connectionState={connectionState}
|
|
84
|
+
selectedIntegration={selectedIntegration}
|
|
85
|
+
connectorData={connectorData?.config ?? null}
|
|
86
|
+
hubData={hubData ?? null}
|
|
87
|
+
fields={fields}
|
|
88
|
+
guide={guide}
|
|
89
|
+
errorHubData={errorHubData}
|
|
90
|
+
errorConnectorData={errorConnectorData}
|
|
91
|
+
onSelect={setSelectedIntegration}
|
|
92
|
+
onChange={setFormData}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
87
95
|
</Card>
|
|
88
96
|
);
|
|
89
97
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Alert, Form, Input, Spacer, Typography } from '@stackone/malachite';
|
|
1
|
+
import { Alert, Dropdown, Form, Input, Spacer, TextArea, Typography } from '@stackone/malachite';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { ConnectorConfigField } from '../types';
|
|
4
4
|
|
|
@@ -29,6 +29,7 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
|
|
|
29
29
|
return initialData;
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
|
32
33
|
useEffect(() => {
|
|
33
34
|
const updatedData: Record<string, string> = {};
|
|
34
35
|
fields.forEach((field) => {
|
|
@@ -37,14 +38,17 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
|
|
|
37
38
|
}
|
|
38
39
|
});
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
setFormData((prev) => {
|
|
42
|
+
const hasChanges =
|
|
43
|
+
Object.keys(updatedData).some((key) => updatedData[key] !== prev[key]) ||
|
|
44
|
+
Object.keys(prev).some((key) => !updatedData.hasOwnProperty(key));
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (hasChanges) {
|
|
47
|
+
return { ...prev, ...updatedData };
|
|
48
|
+
}
|
|
49
|
+
return prev;
|
|
50
|
+
});
|
|
51
|
+
}, [fields.length]);
|
|
48
52
|
|
|
49
53
|
useEffect(() => {
|
|
50
54
|
onChange(formData);
|
|
@@ -58,58 +62,68 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
|
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
return (
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
<Spacer direction="vertical" size={8} fullWidth>
|
|
66
|
+
{guide && <Alert type="info" message={guide?.description} hasMargin={false} />}
|
|
67
|
+
{error && <Alert type="error" message={error.message} hasMargin={false} />}
|
|
68
|
+
{error && <Typography.CodeText>{error.provider_response}</Typography.CodeText>}
|
|
69
|
+
<Spacer direction="vertical" size={20} fullWidth>
|
|
70
|
+
{fields.map((field) => {
|
|
71
|
+
const key =
|
|
72
|
+
typeof field.key === 'object'
|
|
73
|
+
? JSON.stringify(field.key)
|
|
74
|
+
: String(field.key);
|
|
75
|
+
return (
|
|
76
|
+
<div key={key} style={{ width: '100%' }}>
|
|
77
|
+
{(field.type === 'text' ||
|
|
78
|
+
field.type === 'number' ||
|
|
79
|
+
field.type === 'password') && (
|
|
80
|
+
<Input
|
|
81
|
+
name={key}
|
|
82
|
+
required={field.required}
|
|
83
|
+
placeholder={field.placeholder}
|
|
84
|
+
defaultValue={field.value?.toString()}
|
|
85
|
+
onChange={(value: string) => handleFieldChange(key, value)}
|
|
86
|
+
disabled={field.readOnly}
|
|
87
|
+
label={field.label}
|
|
88
|
+
tooltip={field.guide?.tooltip}
|
|
89
|
+
description={field.guide?.description}
|
|
90
|
+
type={field.type}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
94
|
+
{field.type === 'text_area' && (
|
|
95
|
+
<TextArea
|
|
96
|
+
name={key}
|
|
97
|
+
required={field.required}
|
|
98
|
+
defaultValue={formData[key] || ''}
|
|
99
|
+
placeholder={field.placeholder}
|
|
100
|
+
onChange={(value: string) => handleFieldChange(key, value)}
|
|
101
|
+
disabled={field.readOnly}
|
|
102
|
+
label={field.label}
|
|
103
|
+
tooltip={field.guide?.tooltip}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
{field.type === 'select' && (
|
|
107
|
+
<Dropdown
|
|
108
|
+
defaultValue={formData[key] || ''}
|
|
109
|
+
disabled={field.readOnly}
|
|
110
|
+
items={
|
|
111
|
+
field.options?.map((option) => ({
|
|
112
|
+
id: option.value,
|
|
113
|
+
label: option.label,
|
|
114
|
+
})) ?? []
|
|
115
|
+
}
|
|
116
|
+
onItemSelected={(value) => handleFieldChange(key, value ?? '')}
|
|
117
|
+
name={key}
|
|
118
|
+
label={field.label}
|
|
119
|
+
tooltip={field.guide?.tooltip}
|
|
120
|
+
description={field.guide?.description}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
112
126
|
</Spacer>
|
|
113
|
-
</
|
|
127
|
+
</Spacer>
|
|
114
128
|
);
|
|
115
129
|
};
|
|
@@ -32,15 +32,7 @@ const CardFooter: React.FC<CardFooterProps> = ({
|
|
|
32
32
|
onClick: () => void;
|
|
33
33
|
disabled: boolean;
|
|
34
34
|
loading: boolean;
|
|
35
|
-
}> = [
|
|
36
|
-
{
|
|
37
|
-
label: 'Next',
|
|
38
|
-
type: 'filled' as const,
|
|
39
|
-
onClick: onNext,
|
|
40
|
-
disabled: false,
|
|
41
|
-
loading: false,
|
|
42
|
-
},
|
|
43
|
-
];
|
|
35
|
+
}> = [];
|
|
44
36
|
|
|
45
37
|
if (onBack) {
|
|
46
38
|
buttons.push({
|
|
@@ -52,6 +44,14 @@ const CardFooter: React.FC<CardFooterProps> = ({
|
|
|
52
44
|
});
|
|
53
45
|
}
|
|
54
46
|
|
|
47
|
+
buttons.push({
|
|
48
|
+
label: 'Confirm',
|
|
49
|
+
type: 'filled' as const,
|
|
50
|
+
onClick: onNext,
|
|
51
|
+
disabled: false,
|
|
52
|
+
loading: false,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
55
|
return buttons;
|
|
56
56
|
}, [selectedIntegration, onBack, onNext]);
|
|
57
57
|
|
|
@@ -62,14 +62,14 @@ const CardFooter: React.FC<CardFooterProps> = ({
|
|
|
62
62
|
return (
|
|
63
63
|
<Spacer direction="horizontal" size={0} justifyContent="space-between">
|
|
64
64
|
<FooterLinks fullWidth={fullWidth} />
|
|
65
|
-
<Padded vertical="
|
|
65
|
+
<Padded vertical="none" horizontal="medium" fullHeight={false}>
|
|
66
66
|
<Flex direction={FlexDirection.Horizontal} justify={FlexJustify.Right}>
|
|
67
67
|
<Spacer direction="horizontal" size={10}>
|
|
68
68
|
{buttons.map((button) => (
|
|
69
69
|
<Button
|
|
70
70
|
key={button.label}
|
|
71
71
|
size="small"
|
|
72
|
-
|
|
72
|
+
variant={button.type}
|
|
73
73
|
onClick={button.onClick}
|
|
74
74
|
disabled={button.disabled}
|
|
75
75
|
loading={button.loading}
|
|
@@ -29,7 +29,7 @@ const CardTitle: React.FC<CardTitleProps> = ({ selectedIntegration, onBack, guid
|
|
|
29
29
|
gapSize={FlexGapSize.Small}
|
|
30
30
|
justify={FlexJustify.Left}
|
|
31
31
|
>
|
|
32
|
-
{onBack && <Button
|
|
32
|
+
{onBack && <Button variant="ghost" onClick={onBack} icon="←" size="small" />}
|
|
33
33
|
<img
|
|
34
34
|
src={`https://app.stackone.com/assets/logos/${selectedIntegration.provider}.png`}
|
|
35
35
|
alt={selectedIntegration.provider}
|
|
@@ -39,11 +39,13 @@ const CardTitle: React.FC<CardTitleProps> = ({ selectedIntegration, onBack, guid
|
|
|
39
39
|
{selectedIntegration.name}
|
|
40
40
|
</Typography.Text>
|
|
41
41
|
</Flex>
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
{guide?.supportLink && (
|
|
43
|
+
<Typography.LinkText href={guide?.supportLink} target="_blank">
|
|
44
|
+
<Button variant="outline" size="medium">
|
|
45
|
+
Connection guide
|
|
46
|
+
</Button>
|
|
47
|
+
</Typography.LinkText>
|
|
48
|
+
)}
|
|
47
49
|
</Flex>
|
|
48
50
|
);
|
|
49
51
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { evaluate } from '@stackone/expressions';
|
|
1
2
|
import { useQuery } from '@tanstack/react-query';
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
4
|
import {
|
|
4
5
|
connectAccount,
|
|
5
6
|
getAccountData,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
getHubData,
|
|
8
9
|
updateAccount,
|
|
9
10
|
} from '../queries';
|
|
10
|
-
import { Integration } from '../types';
|
|
11
|
+
import { ConnectorConfigField, Integration } from '../types';
|
|
11
12
|
|
|
12
13
|
const DUMMY_VALUE = 'totally-fake-value';
|
|
13
14
|
|
|
@@ -16,6 +17,13 @@ interface UseIntegrationPickerProps {
|
|
|
16
17
|
baseUrl: string;
|
|
17
18
|
accountId?: string;
|
|
18
19
|
onSuccess?: () => void;
|
|
20
|
+
dashboardUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export enum EventType {
|
|
24
|
+
AccountConnected = 'AccountConnected',
|
|
25
|
+
CloseModal = 'CloseModal',
|
|
26
|
+
CloseOAuth2 = 'CloseOAuth2',
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export const useIntegrationPicker = ({
|
|
@@ -23,9 +31,13 @@ export const useIntegrationPicker = ({
|
|
|
23
31
|
baseUrl,
|
|
24
32
|
accountId,
|
|
25
33
|
onSuccess,
|
|
34
|
+
dashboardUrl,
|
|
26
35
|
}: UseIntegrationPickerProps) => {
|
|
27
36
|
const [selectedIntegration, setSelectedIntegration] = useState<Integration | null>(null);
|
|
28
37
|
const [formData, setFormData] = useState<Record<string, string>>({});
|
|
38
|
+
const connectWindow = useRef<Window | null>(null);
|
|
39
|
+
const checkStateTimeoutRef = useRef<number | null>(null);
|
|
40
|
+
const successTimeoutRef = useRef<number | null>(null);
|
|
29
41
|
const [connectionState, setConnectionState] = useState<{
|
|
30
42
|
loading: boolean;
|
|
31
43
|
success: boolean;
|
|
@@ -38,7 +50,48 @@ export const useIntegrationPicker = ({
|
|
|
38
50
|
success: false,
|
|
39
51
|
});
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
return () => {
|
|
55
|
+
if (checkStateTimeoutRef.current !== null) {
|
|
56
|
+
clearTimeout(checkStateTimeoutRef.current);
|
|
57
|
+
}
|
|
58
|
+
if (successTimeoutRef.current !== null) {
|
|
59
|
+
clearTimeout(successTimeoutRef.current);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const processMessageCallback = useCallback((event: MessageEvent) => {
|
|
65
|
+
if (event.data.type === EventType.AccountConnected) {
|
|
66
|
+
setConnectionState({ loading: false, success: true });
|
|
67
|
+
parent.postMessage(event.data, '*');
|
|
68
|
+
if (connectWindow.current) {
|
|
69
|
+
connectWindow.current.close();
|
|
70
|
+
connectWindow.current = null;
|
|
71
|
+
}
|
|
72
|
+
window.removeEventListener('message', processMessageCallback, false);
|
|
73
|
+
} else if (event.data.type === EventType.CloseOAuth2) {
|
|
74
|
+
if (event.data.error) {
|
|
75
|
+
setConnectionState({
|
|
76
|
+
loading: false,
|
|
77
|
+
success: false,
|
|
78
|
+
error: {
|
|
79
|
+
message: event.data.error,
|
|
80
|
+
provider_response: event.data.errorDescription || 'No description',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
setConnectionState({ loading: false, success: false, error: undefined });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (connectWindow.current) {
|
|
88
|
+
connectWindow.current.close();
|
|
89
|
+
connectWindow.current = null;
|
|
90
|
+
}
|
|
91
|
+
window.removeEventListener('message', processMessageCallback, false);
|
|
92
|
+
}
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
42
95
|
const {
|
|
43
96
|
data: accountData,
|
|
44
97
|
isLoading: isLoadingAccountData,
|
|
@@ -52,7 +105,6 @@ export const useIntegrationPicker = ({
|
|
|
52
105
|
enabled: !!accountId,
|
|
53
106
|
});
|
|
54
107
|
|
|
55
|
-
// Fetch hub data (list of integrations)
|
|
56
108
|
const {
|
|
57
109
|
data: hubData,
|
|
58
110
|
isLoading: isLoadingHubData,
|
|
@@ -60,17 +112,14 @@ export const useIntegrationPicker = ({
|
|
|
60
112
|
} = useQuery({
|
|
61
113
|
queryKey: ['hubData', accountData?.provider],
|
|
62
114
|
queryFn: () => {
|
|
63
|
-
// For account editing: fetch hub data with specific provider
|
|
64
115
|
if (accountData?.provider) {
|
|
65
116
|
return getHubData(token, baseUrl, accountData.provider);
|
|
66
117
|
}
|
|
67
|
-
// For new setup: fetch all integrations
|
|
68
118
|
return getHubData(token, baseUrl);
|
|
69
119
|
},
|
|
70
|
-
enabled: !accountId || !!accountData,
|
|
120
|
+
enabled: !accountId || !!accountData,
|
|
71
121
|
});
|
|
72
122
|
|
|
73
|
-
// Auto-select integration when editing an account
|
|
74
123
|
useEffect(() => {
|
|
75
124
|
if (accountData && hubData) {
|
|
76
125
|
const matchingIntegration = hubData.integrations.find(
|
|
@@ -80,7 +129,6 @@ export const useIntegrationPicker = ({
|
|
|
80
129
|
}
|
|
81
130
|
}, [accountData, hubData]);
|
|
82
131
|
|
|
83
|
-
// Fetch connector configuration
|
|
84
132
|
const {
|
|
85
133
|
data: connectorData,
|
|
86
134
|
isLoading: isLoadingConnectorData,
|
|
@@ -99,47 +147,100 @@ export const useIntegrationPicker = ({
|
|
|
99
147
|
enabled: Boolean(selectedIntegration) || Boolean(accountData),
|
|
100
148
|
});
|
|
101
149
|
|
|
102
|
-
// Extract fields and guide from connector config
|
|
103
150
|
const { fields, guide } = useMemo(() => {
|
|
104
151
|
if (!connectorData || !selectedIntegration) {
|
|
105
|
-
|
|
152
|
+
const fields: ConnectorConfigField[] = [];
|
|
153
|
+
return { fields };
|
|
106
154
|
}
|
|
107
155
|
|
|
108
156
|
const authConfig =
|
|
109
|
-
connectorData.authentication?.[selectedIntegration.authentication_config_key];
|
|
157
|
+
connectorData.config.authentication?.[selectedIntegration.authentication_config_key];
|
|
110
158
|
const authConfigForEnvironment = authConfig?.[selectedIntegration.environment];
|
|
111
159
|
|
|
112
160
|
const baseFields = authConfigForEnvironment?.fields || [];
|
|
113
161
|
|
|
114
|
-
const fieldsWithPrefilledValues = baseFields
|
|
115
|
-
|
|
162
|
+
const fieldsWithPrefilledValues: ConnectorConfigField[] = baseFields
|
|
163
|
+
.map((field) => {
|
|
164
|
+
const setupValue = accountData?.setupInformation?.[field.key];
|
|
165
|
+
|
|
166
|
+
if (accountData && (field.secret || field.type === 'password')) {
|
|
167
|
+
return {
|
|
168
|
+
...field,
|
|
169
|
+
key: field.key,
|
|
170
|
+
value: DUMMY_VALUE,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (field.key === 'external-trigger-token') {
|
|
175
|
+
return {
|
|
176
|
+
...field,
|
|
177
|
+
key: field.key,
|
|
178
|
+
value: hubData?.external_trigger_token,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const evaluationContext = {
|
|
183
|
+
...formData,
|
|
184
|
+
...accountData?.setupInformation,
|
|
185
|
+
external_trigger_token: hubData?.external_trigger_token,
|
|
186
|
+
hub_settings: connectorData.hub_settings,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (field.condition) {
|
|
190
|
+
const evaluated = evaluate(field.condition, evaluationContext);
|
|
191
|
+
|
|
192
|
+
const shouldShow = evaluated != null && evaluated !== 'false';
|
|
193
|
+
|
|
194
|
+
if (!shouldShow) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!field.value) {
|
|
200
|
+
return {
|
|
201
|
+
...field,
|
|
202
|
+
key: field.key,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const valueToEvaluate = setupValue !== undefined ? setupValue : field.value;
|
|
207
|
+
let evaluatedValue = evaluate(valueToEvaluate?.toString(), evaluationContext);
|
|
208
|
+
|
|
209
|
+
if (typeof evaluatedValue === 'object' && evaluatedValue !== null) {
|
|
210
|
+
evaluatedValue = JSON.stringify(evaluatedValue);
|
|
211
|
+
}
|
|
116
212
|
|
|
117
|
-
if (accountData && (field.secret || field.type === 'password')) {
|
|
118
213
|
return {
|
|
119
214
|
...field,
|
|
120
|
-
|
|
215
|
+
key: field.key,
|
|
216
|
+
value: evaluatedValue as string | number | undefined,
|
|
121
217
|
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
...field,
|
|
126
|
-
value: setupValue !== undefined ? setupValue : field.value,
|
|
127
|
-
};
|
|
128
|
-
});
|
|
218
|
+
})
|
|
219
|
+
.filter((value) => value != null);
|
|
129
220
|
|
|
130
221
|
return {
|
|
131
222
|
fields: fieldsWithPrefilledValues,
|
|
132
223
|
guide: authConfigForEnvironment?.guide,
|
|
133
224
|
};
|
|
134
|
-
}, [connectorData, selectedIntegration, accountData]);
|
|
225
|
+
}, [connectorData, selectedIntegration, accountData, formData, hubData]);
|
|
226
|
+
|
|
227
|
+
const authConfig = useMemo(() => {
|
|
228
|
+
if (!connectorData || !selectedIntegration) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return connectorData.config.authentication?.[
|
|
232
|
+
selectedIntegration.authentication_config_key
|
|
233
|
+
]?.[selectedIntegration.environment];
|
|
234
|
+
}, [connectorData, selectedIntegration]);
|
|
135
235
|
|
|
136
236
|
const handleConnect = useCallback(async () => {
|
|
137
|
-
if (!selectedIntegration)
|
|
237
|
+
if (!selectedIntegration) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
138
240
|
|
|
139
241
|
setConnectionState({ loading: true, success: false });
|
|
140
242
|
|
|
141
243
|
try {
|
|
142
|
-
// Clean up dummy values for secret fields before submission
|
|
143
244
|
const cleanedFormData = { ...formData };
|
|
144
245
|
if (accountData) {
|
|
145
246
|
fields.forEach((field) => {
|
|
@@ -152,6 +253,62 @@ export const useIntegrationPicker = ({
|
|
|
152
253
|
});
|
|
153
254
|
}
|
|
154
255
|
|
|
256
|
+
if (authConfig?.type === 'oauth2') {
|
|
257
|
+
window.addEventListener('message', processMessageCallback, false);
|
|
258
|
+
const callbackEmbeddedAccountsUrl = encodeURIComponent(
|
|
259
|
+
`${dashboardUrl}/embedded/accounts/callback`,
|
|
260
|
+
);
|
|
261
|
+
let windowUrl = `${baseUrl}/connect/oauth2/${selectedIntegration.provider}?redirect_uri=${callbackEmbeddedAccountsUrl}&token=${token}`;
|
|
262
|
+
|
|
263
|
+
Object.keys(cleanedFormData).forEach((key) => {
|
|
264
|
+
windowUrl += `&${key}=${encodeURIComponent(cleanedFormData[key])}`;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const width = 1024;
|
|
268
|
+
const height = 800;
|
|
269
|
+
const screenX =
|
|
270
|
+
typeof window.screenX != 'undefined' ? window.screenX : window.screenLeft;
|
|
271
|
+
const screenY =
|
|
272
|
+
typeof window.screenY != 'undefined' ? window.screenY : window.screenTop;
|
|
273
|
+
const outerWidth =
|
|
274
|
+
typeof window.outerWidth != 'undefined'
|
|
275
|
+
? window.outerWidth
|
|
276
|
+
: document.body.clientWidth;
|
|
277
|
+
const outerHeight =
|
|
278
|
+
typeof window.outerHeight != 'undefined'
|
|
279
|
+
? window.outerHeight
|
|
280
|
+
: document.body.clientHeight - 22;
|
|
281
|
+
const left = screenX + (outerWidth - width) / 2;
|
|
282
|
+
const top = screenY + (outerHeight - height) / 2.5;
|
|
283
|
+
const features =
|
|
284
|
+
'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top;
|
|
285
|
+
|
|
286
|
+
connectWindow.current = window.open(windowUrl, 'Connect Account', features);
|
|
287
|
+
|
|
288
|
+
if (connectWindow.current) {
|
|
289
|
+
connectWindow.current.focus();
|
|
290
|
+
const checkWindowState = () => {
|
|
291
|
+
if (connectWindow.current?.closed) {
|
|
292
|
+
setConnectionState({ loading: false, success: false });
|
|
293
|
+
window.removeEventListener('message', processMessageCallback, false);
|
|
294
|
+
connectWindow.current = null;
|
|
295
|
+
if (checkStateTimeoutRef.current !== null) {
|
|
296
|
+
clearTimeout(checkStateTimeoutRef.current);
|
|
297
|
+
checkStateTimeoutRef.current = null;
|
|
298
|
+
}
|
|
299
|
+
} else if (connectWindow.current) {
|
|
300
|
+
checkStateTimeoutRef.current = window.setTimeout(
|
|
301
|
+
checkWindowState,
|
|
302
|
+
1000,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
checkStateTimeoutRef.current = window.setTimeout(checkWindowState, 1000);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
155
312
|
if (accountId) {
|
|
156
313
|
await updateAccount(
|
|
157
314
|
baseUrl,
|
|
@@ -165,8 +322,12 @@ export const useIntegrationPicker = ({
|
|
|
165
322
|
}
|
|
166
323
|
|
|
167
324
|
setConnectionState({ loading: false, success: true });
|
|
168
|
-
|
|
325
|
+
if (successTimeoutRef.current !== null) {
|
|
326
|
+
clearTimeout(successTimeoutRef.current);
|
|
327
|
+
}
|
|
328
|
+
successTimeoutRef.current = window.setTimeout(() => {
|
|
169
329
|
onSuccess?.();
|
|
330
|
+
successTimeoutRef.current = null;
|
|
170
331
|
}, 2000);
|
|
171
332
|
} catch (error) {
|
|
172
333
|
const parsedError = JSON.parse((error as Error).message) as {
|
|
@@ -188,7 +349,19 @@ export const useIntegrationPicker = ({
|
|
|
188
349
|
},
|
|
189
350
|
});
|
|
190
351
|
}
|
|
191
|
-
}, [
|
|
352
|
+
}, [
|
|
353
|
+
baseUrl,
|
|
354
|
+
dashboardUrl,
|
|
355
|
+
token,
|
|
356
|
+
selectedIntegration,
|
|
357
|
+
formData,
|
|
358
|
+
onSuccess,
|
|
359
|
+
accountData,
|
|
360
|
+
fields,
|
|
361
|
+
accountId,
|
|
362
|
+
authConfig,
|
|
363
|
+
processMessageCallback,
|
|
364
|
+
]);
|
|
192
365
|
|
|
193
366
|
const isLoading = isLoadingHubData || isLoadingConnectorData || isLoadingAccountData;
|
|
194
367
|
const hasError = !!(errorHubData || errorConnectorData || errorAccountData);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getRequest, patchRequest, postRequest } from '../../shared/httpClient';
|
|
2
|
-
import { AccountData, ConnectorConfig, HubData } from './types';
|
|
2
|
+
import { AccountData, ConnectorConfig, HubConnectorConfig, HubData } from './types';
|
|
3
3
|
|
|
4
4
|
export const getHubData = async (token: string, baseUrl: string, provider?: string) => {
|
|
5
5
|
const headers: Record<string, string> = {
|
|
@@ -19,7 +19,7 @@ export const getHubData = async (token: string, baseUrl: string, provider?: stri
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export const getConnectorConfig = async (baseUrl: string, token: string, connectorKey: string) => {
|
|
22
|
-
return await getRequest<
|
|
22
|
+
return await getRequest<HubConnectorConfig>({
|
|
23
23
|
url: `${baseUrl}/hub/connectors/${connectorKey}`,
|
|
24
24
|
headers: {
|
|
25
25
|
'Content-Type': 'application/json',
|
|
@@ -10,6 +10,7 @@ export interface Integration {
|
|
|
10
10
|
|
|
11
11
|
export interface HubData {
|
|
12
12
|
integrations: Array<Integration>;
|
|
13
|
+
external_trigger_token?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface ConnectorConfigField {
|
|
@@ -48,11 +49,20 @@ export interface ConnectorConfig {
|
|
|
48
49
|
supportLink?: string;
|
|
49
50
|
description: string;
|
|
50
51
|
};
|
|
52
|
+
type: 'oauth2' | 'oidc' | 'custom';
|
|
51
53
|
};
|
|
52
54
|
};
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
export interface HubConnectorConfig {
|
|
59
|
+
config: ConnectorConfig;
|
|
60
|
+
hub_settings: {
|
|
61
|
+
configured_webhook_events: Record<string, Set<string>>;
|
|
62
|
+
project_settings: Record<string, string | object>;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
export interface AccountData {
|
|
57
67
|
account_id: string;
|
|
58
68
|
provider: string;
|
|
@@ -6,11 +6,9 @@ import {
|
|
|
6
6
|
FlexGapSize,
|
|
7
7
|
FlexJustify,
|
|
8
8
|
Typography,
|
|
9
|
-
useTheme,
|
|
10
9
|
} from '@stackone/malachite';
|
|
11
10
|
|
|
12
11
|
const ErrorContainer: React.FC = () => {
|
|
13
|
-
const { colors } = useTheme();
|
|
14
12
|
return (
|
|
15
13
|
<Flex
|
|
16
14
|
justify={FlexJustify.Center}
|
|
@@ -19,7 +17,8 @@ const ErrorContainer: React.FC = () => {
|
|
|
19
17
|
gapSize={FlexGapSize.Small}
|
|
20
18
|
fullHeight
|
|
21
19
|
>
|
|
22
|
-
|
|
20
|
+
{/* TODO: fix */}
|
|
21
|
+
<CustomIcons.RejectIcon style={{ color: 'red' }} />
|
|
23
22
|
<Typography.Text fontWeight="bold" size="large">
|
|
24
23
|
Error
|
|
25
24
|
</Typography.Text>
|
|
@@ -6,28 +6,35 @@ import {
|
|
|
6
6
|
FlexGapSize,
|
|
7
7
|
FlexJustify,
|
|
8
8
|
Typography,
|
|
9
|
+
getCurrentTheme,
|
|
9
10
|
} from '@stackone/malachite';
|
|
10
11
|
|
|
11
12
|
interface SuccessProps {
|
|
12
13
|
integrationName: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const Success: React.FC<SuccessProps> = ({ integrationName }) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
const Success: React.FC<SuccessProps> = ({ integrationName }) => {
|
|
17
|
+
const theme = getCurrentTheme();
|
|
18
|
+
return (
|
|
19
|
+
<Flex
|
|
20
|
+
justify={FlexJustify.Center}
|
|
21
|
+
align={FlexAlign.Center}
|
|
22
|
+
direction={FlexDirection.Vertical}
|
|
23
|
+
gapSize={FlexGapSize.Small}
|
|
24
|
+
fullHeight
|
|
25
|
+
>
|
|
26
|
+
<CustomIcons.CheckCircleFilled
|
|
27
|
+
size={16}
|
|
28
|
+
style={{ color: theme.colors.success.foreground }}
|
|
29
|
+
/>
|
|
30
|
+
<Typography.Text fontWeight="bold" size="large">
|
|
31
|
+
Connection Successful
|
|
32
|
+
</Typography.Text>
|
|
33
|
+
<Typography.SecondaryText>
|
|
34
|
+
Account successfully connected to {integrationName}
|
|
35
|
+
</Typography.SecondaryText>
|
|
36
|
+
</Flex>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
32
39
|
|
|
33
40
|
export default Success;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext, useMemo } from 'react';
|
|
2
|
+
import { FeatureFlag } from '../types/featureFlags';
|
|
3
|
+
|
|
4
|
+
export const FeatureFlagContext = createContext<{ featureFlags: FeatureFlag[] }>({
|
|
5
|
+
featureFlags: [],
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const FeatureFlagProvider = ({
|
|
9
|
+
featureFlags,
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
featureFlags: FeatureFlag[];
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}) => {
|
|
15
|
+
const memoizedContextValue = useMemo(
|
|
16
|
+
() => ({
|
|
17
|
+
featureFlags,
|
|
18
|
+
}),
|
|
19
|
+
[featureFlags],
|
|
20
|
+
);
|
|
21
|
+
return (
|
|
22
|
+
<FeatureFlagContext.Provider value={memoizedContextValue}>
|
|
23
|
+
{children}
|
|
24
|
+
</FeatureFlagContext.Provider>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { FeatureFlagContext } from '../contexts/featureFlagContext';
|
|
3
|
+
import { FeatureFlag } from '../types/featureFlags';
|
|
4
|
+
|
|
5
|
+
const isFeatureEnabled = ({
|
|
6
|
+
featureFlags,
|
|
7
|
+
featureFlag,
|
|
8
|
+
}: { featureFlags: FeatureFlag[]; featureFlag: FeatureFlag }): boolean => {
|
|
9
|
+
return featureFlags.includes(featureFlag);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const useFeatureFlags = (featureFlag: FeatureFlag): boolean => {
|
|
13
|
+
const { featureFlags } = useContext(FeatureFlagContext);
|
|
14
|
+
if (!featureFlags) {
|
|
15
|
+
throw new Error('useFeatureFlags must be used within a FeatureFlagProvider');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return isFeatureEnabled({
|
|
19
|
+
featureFlags,
|
|
20
|
+
featureFlag,
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default useFeatureFlags;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getRequest } from './httpClient';
|
|
2
|
+
import { FeatureFlag } from './types/featureFlags';
|
|
3
|
+
|
|
4
|
+
export const getSettings = async (baseUrl: string, token: string) => {
|
|
5
|
+
return await getRequest<{ enabled_features: FeatureFlag[] }>({
|
|
6
|
+
url: `${baseUrl}/hub/settings`,
|
|
7
|
+
headers: {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
'x-hub-session-token': token,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type FeatureFlag = 'hub_link_account_release';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Coming soon
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@stackone/malachite",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"type": "module",
|
|
5
|
-
"module": "dist/index.js",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"files": [
|
|
9
|
-
"dist",
|
|
10
|
-
"README.md",
|
|
11
|
-
"LICENSE"
|
|
12
|
-
],
|
|
13
|
-
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
"types": "./dist/index.d.ts",
|
|
16
|
-
"require": "./dist/index.js",
|
|
17
|
-
"default": "./dist/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./style.css": "./dist/style.css"
|
|
20
|
-
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"dev": "vite",
|
|
23
|
-
"build": "tsc -b ./tsconfig.app.json && vite build",
|
|
24
|
-
"build:watch": "vite build --watch",
|
|
25
|
-
"build:hot": "concurrently \"tsc -b ./tsconfig.app.json --watch\" \"vite build --watch\"",
|
|
26
|
-
"lint:check": "biome lint",
|
|
27
|
-
"lint:fix": "biome lint --fix",
|
|
28
|
-
"preview": "vite preview",
|
|
29
|
-
"storybook": "storybook dev -p 6006",
|
|
30
|
-
"build-storybook": "storybook build"
|
|
31
|
-
},
|
|
32
|
-
"peerDependencies": {
|
|
33
|
-
"react": "^18.0.0",
|
|
34
|
-
"react-dom": "^18.1.0"
|
|
35
|
-
},
|
|
36
|
-
"yalcSig": "b3621bd0c8195b6fd4451f514c72a618"
|
|
37
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
b3621bd0c8195b6fd4451f514c72a618
|