@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
package/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<script src="dist/webcomponent/StackOneHub.web.js"></script>
|
|
5
|
+
</head>
|
|
6
|
+
<body>
|
|
7
|
+
<!-- Add a -->
|
|
8
|
+
<stackone-hub
|
|
9
|
+
token="eyJyZWdpb25fc2x1ZyI6ImV1IiwidG9rZW4iOiJzclgzU0xUemttcmpWQWQtWTdNZXFUTUh3OV9KaVdNRFk3M3RaenRwdVZIVV9EZERmQU14NGF3X3hWb09wdXZuRkQ1OW11WlJjZlZBRDRQMUxRcmVRdyJ9"
|
|
10
|
+
base-url="http://localhost:4000"
|
|
11
|
+
mode="integration-picker"
|
|
12
|
+
></stackone-hub>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stackone/hub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "StackOne HUB",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"build": "rollup -c",
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"code:format": "biome format ./src ./dev",
|
|
11
|
+
"code:format:fix": "biome format --write ./src ./dev",
|
|
12
|
+
"lint": "biome lint --error-on-warnings ./src ./dev && biome check ./src ./dev",
|
|
13
|
+
"lint:fix": "biome lint --write ./src ./dev && biome check --write ./src ./dev",
|
|
14
|
+
"publish-release": "npm publish --access=public"
|
|
15
|
+
},
|
|
16
|
+
"author": "StackOne",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@stackone/malachite": "^0.1.1",
|
|
20
|
+
"@tanstack/react-query": "^5.77.2",
|
|
21
|
+
"react": "^18.0.0",
|
|
22
|
+
"react-dom": "^18.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@biomejs/biome": "1.9.4",
|
|
26
|
+
"@rollup/plugin-commonjs": "^28.0.3",
|
|
27
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
28
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
29
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
30
|
+
"@rollup/plugin-typescript": "^12.1.2",
|
|
31
|
+
"@types/node": "^22.15.17",
|
|
32
|
+
"@types/react": "^18.0.0",
|
|
33
|
+
"@types/react-dom": "^18.0.0",
|
|
34
|
+
"@vitejs/plugin-react-swc": "^3.9.0",
|
|
35
|
+
"react-to-webcomponent": "^2.0.1",
|
|
36
|
+
"rollup": "^4.40.2",
|
|
37
|
+
"rollup-plugin-delete": "^3.0.1",
|
|
38
|
+
"rollup-plugin-dts": "^6.2.1",
|
|
39
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
40
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
41
|
+
"tslib": "^2.8.1",
|
|
42
|
+
"typescript": "^5.8.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import commonjs from "@rollup/plugin-commonjs";
|
|
2
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
3
|
+
import replace from "@rollup/plugin-replace";
|
|
4
|
+
import terser from "@rollup/plugin-terser";
|
|
5
|
+
import typescript from "@rollup/plugin-typescript";
|
|
6
|
+
import del from "rollup-plugin-delete";
|
|
7
|
+
import dts from "rollup-plugin-dts";
|
|
8
|
+
import external from "rollup-plugin-peer-deps-external";
|
|
9
|
+
import postcss from "rollup-plugin-postcss";
|
|
10
|
+
|
|
11
|
+
export default [
|
|
12
|
+
// React Component Bundle (external React)
|
|
13
|
+
{
|
|
14
|
+
input: "src/index.ts",
|
|
15
|
+
output: [
|
|
16
|
+
{
|
|
17
|
+
file: "dist/react/StackOneHub.esm.js",
|
|
18
|
+
format: "esm",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
file: "dist/react/StackOneHub.cjs.js",
|
|
22
|
+
format: "cjs",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
plugins: [
|
|
26
|
+
del({ targets: "dist/react/*" }),
|
|
27
|
+
external(),
|
|
28
|
+
resolve(),
|
|
29
|
+
commonjs(),
|
|
30
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
31
|
+
postcss(),
|
|
32
|
+
terser(),
|
|
33
|
+
replace({
|
|
34
|
+
preventAssignment: true,
|
|
35
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Web Component Bundle (bundled React)
|
|
41
|
+
{
|
|
42
|
+
input: "src/WebComponentWrapper.tsx",
|
|
43
|
+
output: {
|
|
44
|
+
file: "dist/webcomponent/StackOneHub.web.js",
|
|
45
|
+
format: "iife",
|
|
46
|
+
name: "StackOneHubWebComponent",
|
|
47
|
+
sourcemap: true,
|
|
48
|
+
},
|
|
49
|
+
plugins: [
|
|
50
|
+
del({ targets: "dist/webcomponent/*" }), // Clean the dist folder before each build
|
|
51
|
+
resolve(),
|
|
52
|
+
commonjs(),
|
|
53
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
54
|
+
postcss(),
|
|
55
|
+
terser(),
|
|
56
|
+
replace({
|
|
57
|
+
preventAssignment: true,
|
|
58
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Declaration file bundle
|
|
64
|
+
{
|
|
65
|
+
input: "src/index.ts",
|
|
66
|
+
output: {
|
|
67
|
+
file: "dist/index.d.ts",
|
|
68
|
+
format: "es",
|
|
69
|
+
},
|
|
70
|
+
plugins: [del({ targets: "dist/index.d.ts" }), dts()],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Card,
|
|
3
|
+
Flex,
|
|
4
|
+
FlexAlign,
|
|
5
|
+
FlexJustify,
|
|
6
|
+
FooterLinks,
|
|
7
|
+
ThemeProvider,
|
|
8
|
+
Typography,
|
|
9
|
+
} from '@stackone/malachite';
|
|
10
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
11
|
+
import { CsvImporter } from './modules/csv-importer.tsx/CsvImporter';
|
|
12
|
+
import { IntegrationPicker } from './modules/integration-picker/IntegrationPicker';
|
|
13
|
+
import ErrorContainer from './shared/components/error';
|
|
14
|
+
import ErrorBoundary from './shared/components/errorBoundary';
|
|
15
|
+
import { HubModes } from './types/types';
|
|
16
|
+
|
|
17
|
+
interface StackOneHubProps {
|
|
18
|
+
mode?: HubModes;
|
|
19
|
+
token?: string;
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
height?: string;
|
|
22
|
+
theme?: 'light' | 'dark';
|
|
23
|
+
accountId?: string;
|
|
24
|
+
onSuccess?: () => void;
|
|
25
|
+
onClose?: () => void;
|
|
26
|
+
onCancel?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const StackOneHub: React.FC<StackOneHubProps> = ({
|
|
30
|
+
mode,
|
|
31
|
+
token,
|
|
32
|
+
baseUrl,
|
|
33
|
+
height = '500px',
|
|
34
|
+
theme = 'light',
|
|
35
|
+
accountId,
|
|
36
|
+
onSuccess,
|
|
37
|
+
onClose,
|
|
38
|
+
onCancel,
|
|
39
|
+
}) => {
|
|
40
|
+
const defaultBaseUrl = 'https://api.stackone.com';
|
|
41
|
+
const apiUrl = baseUrl ?? defaultBaseUrl;
|
|
42
|
+
|
|
43
|
+
const queryClient = new QueryClient({
|
|
44
|
+
defaultOptions: {
|
|
45
|
+
queries: {
|
|
46
|
+
refetchOnWindowFocus: false,
|
|
47
|
+
retry: 1,
|
|
48
|
+
retryDelay: 500,
|
|
49
|
+
refetchOnMount: false,
|
|
50
|
+
retryOnMount: false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!token) {
|
|
56
|
+
return (
|
|
57
|
+
<Card height={height} footer={<FooterLinks />}>
|
|
58
|
+
<Flex justify={FlexJustify.Center} align={FlexAlign.Center} fullHeight>
|
|
59
|
+
<Typography.PageTitle>No token provided</Typography.PageTitle>
|
|
60
|
+
</Flex>
|
|
61
|
+
</Card>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (!mode) {
|
|
65
|
+
return (
|
|
66
|
+
<Card height={height} footer={<FooterLinks />}>
|
|
67
|
+
<Flex justify={FlexJustify.Center} align={FlexAlign.Center} fullHeight>
|
|
68
|
+
<Typography.PageTitle>No mode selected</Typography.PageTitle>
|
|
69
|
+
</Flex>
|
|
70
|
+
</Card>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<ThemeProvider theme={theme}>
|
|
76
|
+
<ErrorBoundary
|
|
77
|
+
fallback={
|
|
78
|
+
<Card height={height}>
|
|
79
|
+
<ErrorContainer />
|
|
80
|
+
</Card>
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
<QueryClientProvider client={queryClient}>
|
|
84
|
+
{mode === 'integration-picker' && (
|
|
85
|
+
<IntegrationPicker
|
|
86
|
+
token={token}
|
|
87
|
+
baseUrl={apiUrl}
|
|
88
|
+
height={height}
|
|
89
|
+
onSuccess={onSuccess}
|
|
90
|
+
onClose={onClose}
|
|
91
|
+
onCancel={onCancel}
|
|
92
|
+
accountId={accountId}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
</QueryClientProvider>
|
|
96
|
+
</ErrorBoundary>
|
|
97
|
+
</ThemeProvider>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import reactToWebComponent from 'react-to-webcomponent';
|
|
4
|
+
import { StackOneHub } from './StackOneHub';
|
|
5
|
+
|
|
6
|
+
const WebComponent = reactToWebComponent(StackOneHub, React, ReactDOM, {
|
|
7
|
+
props: {
|
|
8
|
+
token: 'string',
|
|
9
|
+
baseUrl: 'string',
|
|
10
|
+
mode: 'string',
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
customElements.define('stackone-hub', WebComponent);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StackOneHub } from './StackOneHub';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Card,
|
|
3
|
+
Flex,
|
|
4
|
+
FlexDirection,
|
|
5
|
+
FlexGapSize,
|
|
6
|
+
FooterLinks,
|
|
7
|
+
Typography,
|
|
8
|
+
} from '@stackone/malachite';
|
|
9
|
+
|
|
10
|
+
interface CsvImporterProps {
|
|
11
|
+
height?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CsvImporter: React.FC<CsvImporterProps> = ({ height }) => {
|
|
15
|
+
return (
|
|
16
|
+
<Card
|
|
17
|
+
title={
|
|
18
|
+
<Typography.Text fontWeight="semi-bold" size="large">
|
|
19
|
+
CSV Importer
|
|
20
|
+
</Typography.Text>
|
|
21
|
+
}
|
|
22
|
+
footer={<FooterLinks />}
|
|
23
|
+
height={height}
|
|
24
|
+
>
|
|
25
|
+
<Flex direction={FlexDirection.Vertical} gapSize={FlexGapSize.Small} fullHeight>
|
|
26
|
+
<Typography.Text fontWeight="bold" size="large">
|
|
27
|
+
CSV Importer
|
|
28
|
+
</Typography.Text>
|
|
29
|
+
<Typography.SecondaryText>
|
|
30
|
+
This is the CSV importer module.
|
|
31
|
+
</Typography.SecondaryText>
|
|
32
|
+
</Flex>
|
|
33
|
+
</Card>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Card } from '@stackone/malachite';
|
|
2
|
+
import { IntegrationPickerContent } from './components/IntegrationPickerContent';
|
|
3
|
+
import CardFooter from './components/cardFooter';
|
|
4
|
+
import CardTitle from './components/cardTitle';
|
|
5
|
+
import { useIntegrationPicker } from './hooks/useIntegrationPicker';
|
|
6
|
+
|
|
7
|
+
interface IntegrationPickerProps {
|
|
8
|
+
token: string;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
height: string;
|
|
11
|
+
accountId?: string;
|
|
12
|
+
onSuccess?: () => void;
|
|
13
|
+
onClose?: () => void;
|
|
14
|
+
onCancel?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const IntegrationPicker: React.FC<IntegrationPickerProps> = ({
|
|
18
|
+
token,
|
|
19
|
+
baseUrl,
|
|
20
|
+
height,
|
|
21
|
+
accountId,
|
|
22
|
+
onSuccess,
|
|
23
|
+
}) => {
|
|
24
|
+
const {
|
|
25
|
+
// Data
|
|
26
|
+
hubData,
|
|
27
|
+
accountData,
|
|
28
|
+
connectorData,
|
|
29
|
+
selectedIntegration,
|
|
30
|
+
fields,
|
|
31
|
+
guide,
|
|
32
|
+
|
|
33
|
+
// State
|
|
34
|
+
connectionState,
|
|
35
|
+
isLoading,
|
|
36
|
+
hasError,
|
|
37
|
+
|
|
38
|
+
// Errors
|
|
39
|
+
errorHubData,
|
|
40
|
+
errorConnectorData,
|
|
41
|
+
|
|
42
|
+
// Actions
|
|
43
|
+
setSelectedIntegration,
|
|
44
|
+
setFormData,
|
|
45
|
+
handleConnect,
|
|
46
|
+
} = useIntegrationPicker({
|
|
47
|
+
token,
|
|
48
|
+
baseUrl,
|
|
49
|
+
accountId,
|
|
50
|
+
onSuccess,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Card
|
|
55
|
+
footer={
|
|
56
|
+
<CardFooter
|
|
57
|
+
selectedIntegration={selectedIntegration}
|
|
58
|
+
onBack={accountData ? undefined : () => setSelectedIntegration(null)}
|
|
59
|
+
onNext={handleConnect}
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
title={
|
|
63
|
+
selectedIntegration && (
|
|
64
|
+
<CardTitle
|
|
65
|
+
selectedIntegration={selectedIntegration}
|
|
66
|
+
onBack={accountData ? undefined : () => setSelectedIntegration(null)}
|
|
67
|
+
guide={guide}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
height={height}
|
|
72
|
+
>
|
|
73
|
+
<IntegrationPickerContent
|
|
74
|
+
isLoading={isLoading}
|
|
75
|
+
hasError={hasError}
|
|
76
|
+
connectionState={connectionState}
|
|
77
|
+
selectedIntegration={selectedIntegration}
|
|
78
|
+
connectorData={connectorData ?? null}
|
|
79
|
+
hubData={hubData ?? null}
|
|
80
|
+
fields={fields}
|
|
81
|
+
guide={guide}
|
|
82
|
+
errorHubData={errorHubData}
|
|
83
|
+
errorConnectorData={errorConnectorData}
|
|
84
|
+
onSelect={setSelectedIntegration}
|
|
85
|
+
onChange={setFormData}
|
|
86
|
+
/>
|
|
87
|
+
</Card>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Alert, Form, Input, Spacer, Typography } from '@stackone/malachite';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { ConnectorConfigField } from '../types';
|
|
4
|
+
|
|
5
|
+
interface IntegrationFieldsProps {
|
|
6
|
+
fields: Array<ConnectorConfigField>;
|
|
7
|
+
guide?: { supportLink?: string; description: string };
|
|
8
|
+
error?: {
|
|
9
|
+
message: string;
|
|
10
|
+
provider_response: string;
|
|
11
|
+
};
|
|
12
|
+
onChange: (data: Record<string, string>) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
|
|
16
|
+
fields,
|
|
17
|
+
guide,
|
|
18
|
+
onChange,
|
|
19
|
+
error,
|
|
20
|
+
}) => {
|
|
21
|
+
// Initialize formData with default values from fields
|
|
22
|
+
const [formData, setFormData] = useState<Record<string, string>>(() => {
|
|
23
|
+
const initialData: Record<string, string> = {};
|
|
24
|
+
fields.forEach((field) => {
|
|
25
|
+
if (field.value !== undefined) {
|
|
26
|
+
initialData[field.key] = field.value.toString();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return initialData;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const updatedData: Record<string, string> = {};
|
|
34
|
+
fields.forEach((field) => {
|
|
35
|
+
if (field.value !== undefined) {
|
|
36
|
+
updatedData[field.key] = field.value.toString();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const hasChanges =
|
|
41
|
+
Object.keys(updatedData).some((key) => updatedData[key] !== formData[key]) ||
|
|
42
|
+
Object.keys(formData).some((key) => !updatedData.hasOwnProperty(key));
|
|
43
|
+
|
|
44
|
+
if (hasChanges) {
|
|
45
|
+
setFormData((prev) => ({ ...prev, ...updatedData }));
|
|
46
|
+
}
|
|
47
|
+
}, [fields, formData]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
onChange(formData);
|
|
51
|
+
}, [formData, onChange]);
|
|
52
|
+
|
|
53
|
+
const handleFieldChange = (key: string, value: string) => {
|
|
54
|
+
setFormData((prev) => ({
|
|
55
|
+
...prev,
|
|
56
|
+
[key]: value,
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<Spacer direction="vertical" size={8}>
|
|
63
|
+
{guide && <Alert type="info" message={guide?.description} hasMargin={false} />}
|
|
64
|
+
{error && <Alert type="error" message={error.message} hasMargin={false} />}
|
|
65
|
+
{error && <Typography.CodeText>{error.provider_response}</Typography.CodeText>}
|
|
66
|
+
<Form>
|
|
67
|
+
<Spacer direction="vertical" size={20}>
|
|
68
|
+
{fields.map((field) => {
|
|
69
|
+
return (
|
|
70
|
+
<div key={field.key}>
|
|
71
|
+
{(field.type === 'text' ||
|
|
72
|
+
field.type === 'number' ||
|
|
73
|
+
field.type === 'password' ||
|
|
74
|
+
field.type === 'text_area') && (
|
|
75
|
+
<Input
|
|
76
|
+
name={field.key}
|
|
77
|
+
required={field.required}
|
|
78
|
+
placeholder={field.placeholder}
|
|
79
|
+
defaultValue={field.value?.toString()}
|
|
80
|
+
onChange={(value) =>
|
|
81
|
+
handleFieldChange(field.key, value)
|
|
82
|
+
}
|
|
83
|
+
disabled={field.readOnly}
|
|
84
|
+
label={field.label}
|
|
85
|
+
tooltip={field.guide?.tooltip}
|
|
86
|
+
description={field.guide?.description}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{field.type === 'select' && (
|
|
91
|
+
<select
|
|
92
|
+
name={field.key}
|
|
93
|
+
required={field.required}
|
|
94
|
+
value={formData[field.key] || ''}
|
|
95
|
+
onChange={(e) =>
|
|
96
|
+
handleFieldChange(field.key, e.target.value)
|
|
97
|
+
}
|
|
98
|
+
disabled={field.readOnly}
|
|
99
|
+
>
|
|
100
|
+
{field.options?.map((option) => (
|
|
101
|
+
<option key={option.value} value={option.value}>
|
|
102
|
+
{option.label}
|
|
103
|
+
</option>
|
|
104
|
+
))}
|
|
105
|
+
</select>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</Spacer>
|
|
111
|
+
</Form>
|
|
112
|
+
</Spacer>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ButtonList,
|
|
3
|
+
Flex,
|
|
4
|
+
FlexAlign,
|
|
5
|
+
FlexDirection,
|
|
6
|
+
FlexGapSize,
|
|
7
|
+
FlexJustify,
|
|
8
|
+
Padded,
|
|
9
|
+
Typography,
|
|
10
|
+
} from '@stackone/malachite';
|
|
11
|
+
import { CATEGORIES_WITH_LABELS } from '../../../shared/categories';
|
|
12
|
+
import { Integration } from '../types';
|
|
13
|
+
|
|
14
|
+
interface IntegrationRowProps {
|
|
15
|
+
integration: Integration;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const IntegrationRow: React.FC<IntegrationRowProps> = ({ integration }) => {
|
|
19
|
+
return (
|
|
20
|
+
<Flex
|
|
21
|
+
direction={FlexDirection.Horizontal}
|
|
22
|
+
align={FlexAlign.Center}
|
|
23
|
+
gapSize={FlexGapSize.Small}
|
|
24
|
+
justify={FlexJustify.SpaceBetween}
|
|
25
|
+
width="100%"
|
|
26
|
+
>
|
|
27
|
+
<Flex
|
|
28
|
+
direction={FlexDirection.Horizontal}
|
|
29
|
+
align={FlexAlign.Center}
|
|
30
|
+
gapSize={FlexGapSize.Small}
|
|
31
|
+
justify={FlexJustify.Left}
|
|
32
|
+
width="100%"
|
|
33
|
+
>
|
|
34
|
+
<img
|
|
35
|
+
src={`https://app.stackone.com/assets/logos/${integration.provider}.png`}
|
|
36
|
+
alt={integration.provider}
|
|
37
|
+
style={{ width: '24px', height: '24px' }}
|
|
38
|
+
/>
|
|
39
|
+
<Typography.Text>{integration.name ?? 'N/A'}</Typography.Text>
|
|
40
|
+
</Flex>
|
|
41
|
+
<Typography.SecondaryText>
|
|
42
|
+
{
|
|
43
|
+
CATEGORIES_WITH_LABELS.find((category) => category.value === integration.type)
|
|
44
|
+
?.label
|
|
45
|
+
}
|
|
46
|
+
</Typography.SecondaryText>
|
|
47
|
+
</Flex>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const IntegrationList: React.FC<{
|
|
52
|
+
integrations: Integration[];
|
|
53
|
+
onSelect: (integration: Integration) => void;
|
|
54
|
+
}> = ({ integrations, onSelect }) => {
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<Padded vertical="medium" horizontal="small" fullHeight={false}>
|
|
58
|
+
<Typography.SecondaryText>Select integration</Typography.SecondaryText>
|
|
59
|
+
</Padded>
|
|
60
|
+
<ButtonList
|
|
61
|
+
buttons={integrations
|
|
62
|
+
?.filter((integration) => integration.active && integration.name)
|
|
63
|
+
.map((integration) => ({
|
|
64
|
+
key: integration.provider,
|
|
65
|
+
children: <IntegrationRow integration={integration} />,
|
|
66
|
+
onClick: () => onSelect(integration),
|
|
67
|
+
}))}
|
|
68
|
+
/>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ConnectorConfig, ConnectorConfigField, HubData, Integration } from '../types';
|
|
3
|
+
import { ErrorView } from './views/ErrorView';
|
|
4
|
+
import { IntegrationFormView } from './views/IntegrationFormView';
|
|
5
|
+
import { IntegrationListView } from './views/IntegrationListView';
|
|
6
|
+
import { LoadingView } from './views/LoadingView';
|
|
7
|
+
import { SuccessView } from './views/SuccessView';
|
|
8
|
+
|
|
9
|
+
interface IntegrationPickerContentProps {
|
|
10
|
+
// State flags
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
hasError: boolean;
|
|
13
|
+
connectionState: {
|
|
14
|
+
loading: boolean;
|
|
15
|
+
success: boolean;
|
|
16
|
+
error?: {
|
|
17
|
+
message: string;
|
|
18
|
+
provider_response: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Data
|
|
23
|
+
selectedIntegration: Integration | null;
|
|
24
|
+
connectorData: ConnectorConfig | null;
|
|
25
|
+
hubData: HubData | null;
|
|
26
|
+
fields: ConnectorConfigField[];
|
|
27
|
+
guide?: { supportLink?: string; description: string };
|
|
28
|
+
|
|
29
|
+
// Errors
|
|
30
|
+
errorHubData: Error | null;
|
|
31
|
+
errorConnectorData: Error | null;
|
|
32
|
+
|
|
33
|
+
// Actions
|
|
34
|
+
onSelect: (integration: Integration) => void;
|
|
35
|
+
onChange: (data: Record<string, string>) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const IntegrationPickerContent: React.FC<IntegrationPickerContentProps> = ({
|
|
39
|
+
isLoading,
|
|
40
|
+
hasError,
|
|
41
|
+
connectionState,
|
|
42
|
+
selectedIntegration,
|
|
43
|
+
connectorData,
|
|
44
|
+
hubData,
|
|
45
|
+
fields,
|
|
46
|
+
guide,
|
|
47
|
+
errorHubData,
|
|
48
|
+
errorConnectorData,
|
|
49
|
+
onSelect,
|
|
50
|
+
onChange,
|
|
51
|
+
}) => {
|
|
52
|
+
// Loading states
|
|
53
|
+
if (isLoading) {
|
|
54
|
+
return (
|
|
55
|
+
<LoadingView
|
|
56
|
+
title="Loading integration data"
|
|
57
|
+
description="Please wait, this may take a moment."
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (connectionState.loading && selectedIntegration) {
|
|
63
|
+
return (
|
|
64
|
+
<LoadingView
|
|
65
|
+
title={`Connecting to ${selectedIntegration.name}`}
|
|
66
|
+
description="Please wait, this may take a moment."
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Error states
|
|
72
|
+
if (hasError) {
|
|
73
|
+
return (
|
|
74
|
+
<ErrorView
|
|
75
|
+
message={errorHubData?.message || errorConnectorData?.message || 'Unknown error'}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Success state
|
|
81
|
+
if (connectionState.success && selectedIntegration) {
|
|
82
|
+
return <SuccessView integrationName={selectedIntegration.name} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Integration selection flow
|
|
86
|
+
if (!selectedIntegration) {
|
|
87
|
+
if (!hubData?.integrations.length) {
|
|
88
|
+
return <ErrorView message="No integrations found." />;
|
|
89
|
+
}
|
|
90
|
+
return <IntegrationListView integrations={hubData.integrations} onSelect={onSelect} />;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Form view (when integration is selected and connector data is available)
|
|
94
|
+
if (connectorData) {
|
|
95
|
+
return (
|
|
96
|
+
<IntegrationFormView
|
|
97
|
+
fields={fields}
|
|
98
|
+
error={connectionState.error}
|
|
99
|
+
onChange={onChange}
|
|
100
|
+
guide={guide}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fallback
|
|
106
|
+
return null;
|
|
107
|
+
};
|