@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.
Files changed (48) hide show
  1. package/.github/workflows/node-ci.yml +20 -0
  2. package/.github/workflows/release-please.yml +37 -0
  3. package/.github/workflows/semantic-pull-request.yml +31 -0
  4. package/.nvmrc +1 -0
  5. package/.release-please-manifest.json +1 -0
  6. package/.yalc/@stackone/malachite/README.md +1 -0
  7. package/.yalc/@stackone/malachite/package.json +37 -0
  8. package/.yalc/@stackone/malachite/yalc.sig +1 -0
  9. package/CHANGELOG.md +30 -0
  10. package/README.md +225 -0
  11. package/biome.json +77 -0
  12. package/dev/index.html +11 -0
  13. package/dev/main.css +80 -0
  14. package/dev/main.tsx +96 -0
  15. package/dev/vite-env.d.ts +15 -0
  16. package/index.html +14 -0
  17. package/package.json +44 -0
  18. package/release-please-config.json +5 -0
  19. package/rollup.config.mjs +72 -0
  20. package/src/StackOneHub.tsx +99 -0
  21. package/src/WebComponentWrapper.tsx +14 -0
  22. package/src/index.ts +1 -0
  23. package/src/modules/csv-importer.tsx/CsvImporter.tsx +35 -0
  24. package/src/modules/integration-picker/IntegrationPicker.tsx +89 -0
  25. package/src/modules/integration-picker/components/IntegrationFields.tsx +115 -0
  26. package/src/modules/integration-picker/components/IntegrationList.tsx +71 -0
  27. package/src/modules/integration-picker/components/IntegrationPickerContent.tsx +107 -0
  28. package/src/modules/integration-picker/components/cardFooter.tsx +88 -0
  29. package/src/modules/integration-picker/components/cardTitle.tsx +51 -0
  30. package/src/modules/integration-picker/components/views/ErrorView.tsx +9 -0
  31. package/src/modules/integration-picker/components/views/IntegrationFormView.tsx +22 -0
  32. package/src/modules/integration-picker/components/views/IntegrationListView.tsx +19 -0
  33. package/src/modules/integration-picker/components/views/LoadingView.tsx +11 -0
  34. package/src/modules/integration-picker/components/views/SuccessView.tsx +10 -0
  35. package/src/modules/integration-picker/components/views/index.ts +5 -0
  36. package/src/modules/integration-picker/hooks/useIntegrationPicker.ts +221 -0
  37. package/src/modules/integration-picker/queries.ts +78 -0
  38. package/src/modules/integration-picker/types.ts +60 -0
  39. package/src/shared/categories.ts +55 -0
  40. package/src/shared/components/error.tsx +33 -0
  41. package/src/shared/components/errorBoundary.tsx +31 -0
  42. package/src/shared/components/loading.tsx +30 -0
  43. package/src/shared/components/success.tsx +33 -0
  44. package/src/shared/httpClient.ts +79 -0
  45. package/src/types/types.ts +1 -0
  46. package/tsconfig.json +19 -0
  47. package/vite.config.ts +11 -0
  48. 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,5 @@
1
+ {
2
+ "packages": {
3
+ ".": {}
4
+ }
5
+ }
@@ -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
+ };