@stackone/hub 0.1.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/.github/workflows/node-ci.yml +20 -0
- package/.github/workflows/release-please.yml +37 -0
- package/.github/workflows/semantic-pull-request.yml +31 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +1 -0
- package/.yalc/@stackone/malachite/README.md +1 -0
- package/.yalc/@stackone/malachite/package.json +37 -0
- package/.yalc/@stackone/malachite/yalc.sig +1 -0
- package/CHANGELOG.md +30 -0
- package/README.md +225 -0
- package/biome.json +77 -0
- package/dev/index.html +11 -0
- package/dev/main.css +80 -0
- package/dev/main.tsx +96 -0
- package/dev/vite-env.d.ts +15 -0
- package/index.html +14 -0
- package/package.json +44 -0
- package/release-please-config.json +5 -0
- package/rollup.config.mjs +72 -0
- package/src/StackOneHub.tsx +99 -0
- package/src/WebComponentWrapper.tsx +14 -0
- package/src/index.ts +1 -0
- package/src/modules/csv-importer.tsx/CsvImporter.tsx +35 -0
- package/src/modules/integration-picker/IntegrationPicker.tsx +89 -0
- package/src/modules/integration-picker/components/IntegrationFields.tsx +115 -0
- package/src/modules/integration-picker/components/IntegrationList.tsx +71 -0
- package/src/modules/integration-picker/components/IntegrationPickerContent.tsx +107 -0
- package/src/modules/integration-picker/components/cardFooter.tsx +88 -0
- package/src/modules/integration-picker/components/cardTitle.tsx +51 -0
- package/src/modules/integration-picker/components/views/ErrorView.tsx +9 -0
- package/src/modules/integration-picker/components/views/IntegrationFormView.tsx +22 -0
- package/src/modules/integration-picker/components/views/IntegrationListView.tsx +19 -0
- package/src/modules/integration-picker/components/views/LoadingView.tsx +11 -0
- package/src/modules/integration-picker/components/views/SuccessView.tsx +10 -0
- package/src/modules/integration-picker/components/views/index.ts +5 -0
- package/src/modules/integration-picker/hooks/useIntegrationPicker.ts +221 -0
- package/src/modules/integration-picker/queries.ts +78 -0
- package/src/modules/integration-picker/types.ts +60 -0
- package/src/shared/categories.ts +55 -0
- package/src/shared/components/error.tsx +33 -0
- package/src/shared/components/errorBoundary.tsx +31 -0
- package/src/shared/components/loading.tsx +30 -0
- package/src/shared/components/success.tsx +33 -0
- package/src/shared/httpClient.ts +79 -0
- package/src/types/types.ts +1 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +11 -0
- package/yalc.lock +9 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Flex,
|
|
4
|
+
FlexDirection,
|
|
5
|
+
FlexJustify,
|
|
6
|
+
FooterLinks,
|
|
7
|
+
Padded,
|
|
8
|
+
Spacer,
|
|
9
|
+
} from '@stackone/malachite';
|
|
10
|
+
import { useMemo } from 'react';
|
|
11
|
+
import { Integration } from '../types';
|
|
12
|
+
|
|
13
|
+
interface CardFooterProps {
|
|
14
|
+
selectedIntegration: Integration | null;
|
|
15
|
+
fullWidth?: boolean;
|
|
16
|
+
onBack?: () => void;
|
|
17
|
+
onNext: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CardFooter: React.FC<CardFooterProps> = ({
|
|
21
|
+
fullWidth = true,
|
|
22
|
+
selectedIntegration,
|
|
23
|
+
onBack,
|
|
24
|
+
onNext,
|
|
25
|
+
}) => {
|
|
26
|
+
const buttons = useMemo(() => {
|
|
27
|
+
if (!selectedIntegration) return [];
|
|
28
|
+
|
|
29
|
+
const buttons: Array<{
|
|
30
|
+
label: string;
|
|
31
|
+
type: 'outline' | 'filled';
|
|
32
|
+
onClick: () => void;
|
|
33
|
+
disabled: boolean;
|
|
34
|
+
loading: boolean;
|
|
35
|
+
}> = [
|
|
36
|
+
{
|
|
37
|
+
label: 'Next',
|
|
38
|
+
type: 'filled' as const,
|
|
39
|
+
onClick: onNext,
|
|
40
|
+
disabled: false,
|
|
41
|
+
loading: false,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
if (onBack) {
|
|
46
|
+
buttons.push({
|
|
47
|
+
label: 'Back',
|
|
48
|
+
type: 'outline' as const,
|
|
49
|
+
onClick: onBack,
|
|
50
|
+
disabled: false,
|
|
51
|
+
loading: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return buttons;
|
|
56
|
+
}, [selectedIntegration, onBack, onNext]);
|
|
57
|
+
|
|
58
|
+
if (buttons.length === 0) {
|
|
59
|
+
return <FooterLinks fullWidth={fullWidth} />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Spacer direction="horizontal" size={0} justifyContent="space-between">
|
|
64
|
+
<FooterLinks fullWidth={fullWidth} />
|
|
65
|
+
<Padded vertical="medium" horizontal="medium" fullHeight={false}>
|
|
66
|
+
<Flex direction={FlexDirection.Horizontal} justify={FlexJustify.Right}>
|
|
67
|
+
<Spacer direction="horizontal" size={10}>
|
|
68
|
+
{buttons.map((button) => (
|
|
69
|
+
<Button
|
|
70
|
+
key={button.label}
|
|
71
|
+
size="small"
|
|
72
|
+
type={button.type}
|
|
73
|
+
onClick={button.onClick}
|
|
74
|
+
disabled={button.disabled}
|
|
75
|
+
loading={button.loading}
|
|
76
|
+
iconPosition="end"
|
|
77
|
+
>
|
|
78
|
+
{button.label}
|
|
79
|
+
</Button>
|
|
80
|
+
))}
|
|
81
|
+
</Spacer>
|
|
82
|
+
</Flex>
|
|
83
|
+
</Padded>
|
|
84
|
+
</Spacer>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default CardFooter;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Flex,
|
|
4
|
+
FlexAlign,
|
|
5
|
+
FlexDirection,
|
|
6
|
+
FlexGapSize,
|
|
7
|
+
FlexJustify,
|
|
8
|
+
Typography,
|
|
9
|
+
} from '@stackone/malachite';
|
|
10
|
+
import { Integration } from '../types';
|
|
11
|
+
|
|
12
|
+
interface CardTitleProps {
|
|
13
|
+
selectedIntegration: Integration;
|
|
14
|
+
onBack?: () => void;
|
|
15
|
+
guide?: { supportLink?: string; description: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CardTitle: React.FC<CardTitleProps> = ({ selectedIntegration, onBack, guide }) => {
|
|
19
|
+
return (
|
|
20
|
+
<Flex
|
|
21
|
+
direction={FlexDirection.Horizontal}
|
|
22
|
+
align={FlexAlign.Center}
|
|
23
|
+
gapSize={FlexGapSize.Small}
|
|
24
|
+
justify={FlexJustify.SpaceBetween}
|
|
25
|
+
>
|
|
26
|
+
<Flex
|
|
27
|
+
direction={FlexDirection.Horizontal}
|
|
28
|
+
align={FlexAlign.Center}
|
|
29
|
+
gapSize={FlexGapSize.Small}
|
|
30
|
+
justify={FlexJustify.Left}
|
|
31
|
+
>
|
|
32
|
+
{onBack && <Button type="ghost" onClick={onBack} icon="←" size="small" />}
|
|
33
|
+
<img
|
|
34
|
+
src={`https://app.stackone.com/assets/logos/${selectedIntegration.provider}.png`}
|
|
35
|
+
alt={selectedIntegration.provider}
|
|
36
|
+
style={{ width: '24px', height: '24px' }}
|
|
37
|
+
/>
|
|
38
|
+
<Typography.Text fontWeight="semi-bold" size="large">
|
|
39
|
+
{selectedIntegration.name}
|
|
40
|
+
</Typography.Text>
|
|
41
|
+
</Flex>
|
|
42
|
+
<Typography.Link href={guide?.supportLink} target="_blank">
|
|
43
|
+
<Button type="outline" size="medium">
|
|
44
|
+
Connection guide
|
|
45
|
+
</Button>
|
|
46
|
+
</Typography.Link>
|
|
47
|
+
</Flex>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default CardTitle;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ConnectorConfigField } from '../../types';
|
|
3
|
+
import { IntegrationForm } from '../IntegrationFields';
|
|
4
|
+
|
|
5
|
+
interface IntegrationFormViewProps {
|
|
6
|
+
fields: ConnectorConfigField[];
|
|
7
|
+
error?: {
|
|
8
|
+
message: string;
|
|
9
|
+
provider_response: string;
|
|
10
|
+
};
|
|
11
|
+
guide?: { supportLink?: string; description: string };
|
|
12
|
+
onChange: (data: Record<string, string>) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const IntegrationFormView: React.FC<IntegrationFormViewProps> = ({
|
|
16
|
+
fields,
|
|
17
|
+
error,
|
|
18
|
+
guide,
|
|
19
|
+
onChange,
|
|
20
|
+
}) => {
|
|
21
|
+
return <IntegrationForm fields={fields} error={error} onChange={onChange} guide={guide} />;
|
|
22
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Integration } from '../../types';
|
|
3
|
+
import { IntegrationList } from '../IntegrationList';
|
|
4
|
+
|
|
5
|
+
interface IntegrationListViewProps {
|
|
6
|
+
integrations: Integration[];
|
|
7
|
+
onSelect: (integration: Integration) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const IntegrationListView: React.FC<IntegrationListViewProps> = ({
|
|
11
|
+
integrations,
|
|
12
|
+
onSelect,
|
|
13
|
+
}) => {
|
|
14
|
+
if (!integrations.length) {
|
|
15
|
+
return <div>No integrations found.</div>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return <IntegrationList integrations={integrations} onSelect={onSelect} />;
|
|
19
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Loading } from '../../../../shared/components/loading';
|
|
3
|
+
|
|
4
|
+
interface LoadingViewProps {
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const LoadingView: React.FC<LoadingViewProps> = ({ title, description }) => {
|
|
10
|
+
return <Loading title={title} description={description} />;
|
|
11
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Success from '../../../../shared/components/success';
|
|
3
|
+
|
|
4
|
+
interface SuccessViewProps {
|
|
5
|
+
integrationName: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const SuccessView: React.FC<SuccessViewProps> = ({ integrationName }) => {
|
|
9
|
+
return <Success integrationName={integrationName} />;
|
|
10
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
connectAccount,
|
|
5
|
+
getAccountData,
|
|
6
|
+
getConnectorConfig,
|
|
7
|
+
getHubData,
|
|
8
|
+
updateAccount,
|
|
9
|
+
} from '../queries';
|
|
10
|
+
import { Integration } from '../types';
|
|
11
|
+
|
|
12
|
+
const DUMMY_VALUE = 'totally-fake-value';
|
|
13
|
+
|
|
14
|
+
interface UseIntegrationPickerProps {
|
|
15
|
+
token: string;
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
accountId?: string;
|
|
18
|
+
onSuccess?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useIntegrationPicker = ({
|
|
22
|
+
token,
|
|
23
|
+
baseUrl,
|
|
24
|
+
accountId,
|
|
25
|
+
onSuccess,
|
|
26
|
+
}: UseIntegrationPickerProps) => {
|
|
27
|
+
const [selectedIntegration, setSelectedIntegration] = useState<Integration | null>(null);
|
|
28
|
+
const [formData, setFormData] = useState<Record<string, string>>({});
|
|
29
|
+
const [connectionState, setConnectionState] = useState<{
|
|
30
|
+
loading: boolean;
|
|
31
|
+
success: boolean;
|
|
32
|
+
error?: {
|
|
33
|
+
message: string;
|
|
34
|
+
provider_response: string;
|
|
35
|
+
};
|
|
36
|
+
}>({
|
|
37
|
+
loading: false,
|
|
38
|
+
success: false,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Fetch account data for editing scenario
|
|
42
|
+
const {
|
|
43
|
+
data: accountData,
|
|
44
|
+
isLoading: isLoadingAccountData,
|
|
45
|
+
error: errorAccountData,
|
|
46
|
+
} = useQuery({
|
|
47
|
+
queryKey: ['accountData', accountId],
|
|
48
|
+
queryFn: async () => {
|
|
49
|
+
if (!accountId) return null;
|
|
50
|
+
return getAccountData(baseUrl, token, accountId);
|
|
51
|
+
},
|
|
52
|
+
enabled: !!accountId,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Fetch hub data (list of integrations)
|
|
56
|
+
const {
|
|
57
|
+
data: hubData,
|
|
58
|
+
isLoading: isLoadingHubData,
|
|
59
|
+
error: errorHubData,
|
|
60
|
+
} = useQuery({
|
|
61
|
+
queryKey: ['hubData', accountData?.provider],
|
|
62
|
+
queryFn: () => {
|
|
63
|
+
// For account editing: fetch hub data with specific provider
|
|
64
|
+
if (accountData?.provider) {
|
|
65
|
+
return getHubData(token, baseUrl, accountData.provider);
|
|
66
|
+
}
|
|
67
|
+
// For new setup: fetch all integrations
|
|
68
|
+
return getHubData(token, baseUrl);
|
|
69
|
+
},
|
|
70
|
+
enabled: !accountId || !!accountData, // Enable when no accountId OR when we have account data
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Auto-select integration when editing an account
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (accountData && hubData) {
|
|
76
|
+
const matchingIntegration = hubData.integrations.find(
|
|
77
|
+
(integration) => integration.provider === accountData.provider,
|
|
78
|
+
);
|
|
79
|
+
setSelectedIntegration(matchingIntegration ?? null);
|
|
80
|
+
}
|
|
81
|
+
}, [accountData, hubData]);
|
|
82
|
+
|
|
83
|
+
// Fetch connector configuration
|
|
84
|
+
const {
|
|
85
|
+
data: connectorData,
|
|
86
|
+
isLoading: isLoadingConnectorData,
|
|
87
|
+
error: errorConnectorData,
|
|
88
|
+
} = useQuery({
|
|
89
|
+
queryKey: ['connectorData', selectedIntegration?.provider, accountData?.provider],
|
|
90
|
+
queryFn: async () => {
|
|
91
|
+
if (selectedIntegration) {
|
|
92
|
+
return getConnectorConfig(baseUrl, token, selectedIntegration.provider);
|
|
93
|
+
}
|
|
94
|
+
if (accountData) {
|
|
95
|
+
return getConnectorConfig(baseUrl, token, accountData.provider);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
},
|
|
99
|
+
enabled: Boolean(selectedIntegration) || Boolean(accountData),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Extract fields and guide from connector config
|
|
103
|
+
const { fields, guide } = useMemo(() => {
|
|
104
|
+
if (!connectorData || !selectedIntegration) {
|
|
105
|
+
return { fields: [] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const authConfig =
|
|
109
|
+
connectorData.authentication?.[selectedIntegration.authentication_config_key];
|
|
110
|
+
const authConfigForEnvironment = authConfig?.[selectedIntegration.environment];
|
|
111
|
+
|
|
112
|
+
const baseFields = authConfigForEnvironment?.fields || [];
|
|
113
|
+
|
|
114
|
+
const fieldsWithPrefilledValues = baseFields.map((field) => {
|
|
115
|
+
const setupValue = accountData?.setupInformation?.[field.key];
|
|
116
|
+
|
|
117
|
+
if (accountData && (field.secret || field.type === 'password')) {
|
|
118
|
+
return {
|
|
119
|
+
...field,
|
|
120
|
+
value: DUMMY_VALUE,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
...field,
|
|
126
|
+
value: setupValue !== undefined ? setupValue : field.value,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
fields: fieldsWithPrefilledValues,
|
|
132
|
+
guide: authConfigForEnvironment?.guide,
|
|
133
|
+
};
|
|
134
|
+
}, [connectorData, selectedIntegration, accountData]);
|
|
135
|
+
|
|
136
|
+
const handleConnect = useCallback(async () => {
|
|
137
|
+
if (!selectedIntegration) return;
|
|
138
|
+
|
|
139
|
+
setConnectionState({ loading: true, success: false });
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Clean up dummy values for secret fields before submission
|
|
143
|
+
const cleanedFormData = { ...formData };
|
|
144
|
+
if (accountData) {
|
|
145
|
+
fields.forEach((field) => {
|
|
146
|
+
if (
|
|
147
|
+
(field.secret || field.type === 'password') &&
|
|
148
|
+
cleanedFormData[field.key] === DUMMY_VALUE
|
|
149
|
+
) {
|
|
150
|
+
delete cleanedFormData[field.key];
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (accountId) {
|
|
156
|
+
await updateAccount(
|
|
157
|
+
baseUrl,
|
|
158
|
+
accountId,
|
|
159
|
+
token,
|
|
160
|
+
selectedIntegration.provider,
|
|
161
|
+
cleanedFormData,
|
|
162
|
+
);
|
|
163
|
+
} else {
|
|
164
|
+
await connectAccount(baseUrl, token, selectedIntegration.provider, cleanedFormData);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setConnectionState({ loading: false, success: true });
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
onSuccess?.();
|
|
170
|
+
}, 2000);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const parsedError = JSON.parse((error as Error).message) as {
|
|
173
|
+
status: number;
|
|
174
|
+
message: string;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const doubleParsedError = JSON.parse(parsedError.message) as {
|
|
178
|
+
message: string;
|
|
179
|
+
provider_response: string;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
setConnectionState({
|
|
183
|
+
loading: false,
|
|
184
|
+
success: false,
|
|
185
|
+
error: {
|
|
186
|
+
message: doubleParsedError.message,
|
|
187
|
+
provider_response: doubleParsedError.provider_response,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}, [baseUrl, token, selectedIntegration, formData, onSuccess, accountData, fields, accountId]);
|
|
192
|
+
|
|
193
|
+
const isLoading = isLoadingHubData || isLoadingConnectorData || isLoadingAccountData;
|
|
194
|
+
const hasError = !!(errorHubData || errorConnectorData || errorAccountData);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
// Data
|
|
198
|
+
hubData,
|
|
199
|
+
accountData,
|
|
200
|
+
connectorData,
|
|
201
|
+
selectedIntegration,
|
|
202
|
+
fields,
|
|
203
|
+
guide,
|
|
204
|
+
|
|
205
|
+
// State
|
|
206
|
+
formData,
|
|
207
|
+
connectionState,
|
|
208
|
+
isLoading,
|
|
209
|
+
hasError,
|
|
210
|
+
|
|
211
|
+
// Errors
|
|
212
|
+
errorHubData,
|
|
213
|
+
errorConnectorData,
|
|
214
|
+
errorAccountData,
|
|
215
|
+
|
|
216
|
+
// Actions
|
|
217
|
+
setSelectedIntegration,
|
|
218
|
+
setFormData,
|
|
219
|
+
handleConnect,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getRequest, patchRequest, postRequest } from '../../shared/httpClient';
|
|
2
|
+
import { AccountData, ConnectorConfig, HubData } from './types';
|
|
3
|
+
|
|
4
|
+
export const getHubData = async (token: string, baseUrl: string, provider?: string) => {
|
|
5
|
+
const headers: Record<string, string> = {
|
|
6
|
+
'Content-Type': 'application/json',
|
|
7
|
+
'x-hub-session-token': token,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Add provider header when filtering by specific provider
|
|
11
|
+
if (provider) {
|
|
12
|
+
headers['x-hub-provider'] = provider;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return await getRequest<HubData>({
|
|
16
|
+
url: `${baseUrl}/hub/connectors`,
|
|
17
|
+
headers,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const getConnectorConfig = async (baseUrl: string, token: string, connectorKey: string) => {
|
|
22
|
+
return await getRequest<ConnectorConfig>({
|
|
23
|
+
url: `${baseUrl}/hub/connectors/${connectorKey}`,
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
'x-hub-session-token': token,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const connectAccount = async (
|
|
32
|
+
baseUrl: string,
|
|
33
|
+
token: string,
|
|
34
|
+
provider: string,
|
|
35
|
+
credentials: Record<string, unknown>,
|
|
36
|
+
) => {
|
|
37
|
+
return await postRequest<ConnectorConfig>({
|
|
38
|
+
url: `${baseUrl}/hub/accounts`,
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
'x-hub-session-token': token,
|
|
42
|
+
},
|
|
43
|
+
body: {
|
|
44
|
+
provider,
|
|
45
|
+
credentials,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const updateAccount = async (
|
|
51
|
+
baseUrl: string,
|
|
52
|
+
accountId: string,
|
|
53
|
+
token: string,
|
|
54
|
+
provider: string,
|
|
55
|
+
credentials: Record<string, unknown>,
|
|
56
|
+
) => {
|
|
57
|
+
return await patchRequest<ConnectorConfig>({
|
|
58
|
+
url: `${baseUrl}/hub/accounts/${accountId}`,
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'x-hub-session-token': token,
|
|
62
|
+
},
|
|
63
|
+
body: {
|
|
64
|
+
provider,
|
|
65
|
+
credentials,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const getAccountData = async (baseUrl: string, token: string, accountId: string) => {
|
|
71
|
+
return await getRequest<AccountData>({
|
|
72
|
+
url: `${baseUrl}/hub/accounts/${accountId}`,
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
'x-hub-session-token': token,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface Integration {
|
|
2
|
+
active: boolean;
|
|
3
|
+
name: string;
|
|
4
|
+
provider: string;
|
|
5
|
+
type: string;
|
|
6
|
+
version: string;
|
|
7
|
+
authentication_config_key: string;
|
|
8
|
+
environment: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface HubData {
|
|
12
|
+
integrations: Array<Integration>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ConnectorConfigField {
|
|
16
|
+
type?: 'text' | 'password' | 'number' | 'select' | 'text_area';
|
|
17
|
+
label: string;
|
|
18
|
+
key: string;
|
|
19
|
+
required: boolean;
|
|
20
|
+
readOnly: boolean;
|
|
21
|
+
secret: boolean;
|
|
22
|
+
placeholder: string;
|
|
23
|
+
options?: Array<{
|
|
24
|
+
label: string;
|
|
25
|
+
value: string;
|
|
26
|
+
}>;
|
|
27
|
+
guide?: {
|
|
28
|
+
description: string;
|
|
29
|
+
tooltip: string;
|
|
30
|
+
};
|
|
31
|
+
value?: string | number;
|
|
32
|
+
condition?: string;
|
|
33
|
+
validation?: {
|
|
34
|
+
type: 'html-pattern' | 'domain';
|
|
35
|
+
pattern: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ConnectorConfig {
|
|
41
|
+
key: string;
|
|
42
|
+
name: string;
|
|
43
|
+
authentication: {
|
|
44
|
+
[authKey: string]: {
|
|
45
|
+
[environment: string]: {
|
|
46
|
+
fields: Array<ConnectorConfigField>;
|
|
47
|
+
guide?: {
|
|
48
|
+
supportLink?: string;
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AccountData {
|
|
57
|
+
account_id: string;
|
|
58
|
+
provider: string;
|
|
59
|
+
setupInformation: Record<string, string>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const CATEGORIES = [
|
|
2
|
+
'ats',
|
|
3
|
+
'crm',
|
|
4
|
+
'hris',
|
|
5
|
+
'marketing',
|
|
6
|
+
'iam',
|
|
7
|
+
'lms',
|
|
8
|
+
'documents',
|
|
9
|
+
'ticketing',
|
|
10
|
+
'screening',
|
|
11
|
+
'messaging',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const CATEGORIES_WITH_LABELS = [
|
|
15
|
+
{
|
|
16
|
+
label: 'HRIS',
|
|
17
|
+
value: 'hris',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: 'ATS',
|
|
21
|
+
value: 'ats',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'CRM',
|
|
25
|
+
value: 'crm',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'Marketing',
|
|
29
|
+
value: 'marketing',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'IAM',
|
|
33
|
+
value: 'iam',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: 'LMS',
|
|
37
|
+
value: 'lms',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: 'Documents',
|
|
41
|
+
value: 'documents',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: 'Ticketing',
|
|
45
|
+
value: 'ticketing',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Screening',
|
|
49
|
+
value: 'screening',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: 'Messaging',
|
|
53
|
+
value: 'messaging',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CustomIcons,
|
|
3
|
+
Flex,
|
|
4
|
+
FlexAlign,
|
|
5
|
+
FlexDirection,
|
|
6
|
+
FlexGapSize,
|
|
7
|
+
FlexJustify,
|
|
8
|
+
Typography,
|
|
9
|
+
useTheme,
|
|
10
|
+
} from '@stackone/malachite';
|
|
11
|
+
|
|
12
|
+
const ErrorContainer: React.FC = () => {
|
|
13
|
+
const { colors } = useTheme();
|
|
14
|
+
return (
|
|
15
|
+
<Flex
|
|
16
|
+
justify={FlexJustify.Center}
|
|
17
|
+
align={FlexAlign.Center}
|
|
18
|
+
direction={FlexDirection.Vertical}
|
|
19
|
+
gapSize={FlexGapSize.Small}
|
|
20
|
+
fullHeight
|
|
21
|
+
>
|
|
22
|
+
<CustomIcons.RejectIcon style={{ color: colors.redForeground }} />
|
|
23
|
+
<Typography.Text fontWeight="bold" size="large">
|
|
24
|
+
Error
|
|
25
|
+
</Typography.Text>
|
|
26
|
+
<Typography.SecondaryText>
|
|
27
|
+
Something went wrong, our team has been notified.
|
|
28
|
+
</Typography.SecondaryText>
|
|
29
|
+
</Flex>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default ErrorContainer;
|