Skip to content

Commit a1df06e

Browse files
Add Stepper component with ImportPanel and ExportPanel
Implement the import panel and stub out the export panel, using a single Stepper component and a react context with a reducer for managing the state. Implement the fetch requests to start the import, and also to await the import. Co-Authored by GPT-4 ;)
1 parent b253e10 commit a1df06e

12 files changed

Lines changed: 366 additions & 51 deletions

File tree

examples/nextjs-import-airbyte-github-export-seafowl/components/BaseLayout.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@
3434
/* add additional styles for your content area */
3535
color: var(--text);
3636
background-color: var(--background);
37+
padding: 24px;
3738
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* ExportPanel.module.css */
2+
3+
.exportPanel {
4+
/* Style for export panel will go here */
5+
background: inherit;
6+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import styles from "./ExportPanel.module.css";
2+
import { useStepper } from "./StepperContext";
3+
4+
export const ExportPanel = () => {
5+
const [{ stepperState }] = useStepper();
6+
7+
const disabled =
8+
stepperState !== "import_complete" &&
9+
stepperState !== "awaiting_export" &&
10+
stepperState !== "export_complete";
11+
12+
// We will fill this in later
13+
14+
return (
15+
<div className={styles.exportPanel}>
16+
{disabled ? "Export disabled" : "Export..."}
17+
</div>
18+
);
19+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useEffect } from "react";
2+
import { useStepper } from "./StepperContext";
3+
4+
type ImportLoadingBarProps = {
5+
taskId: string;
6+
splitgraphNamespace: string;
7+
splitgraphRepository: string;
8+
};
9+
10+
export const ImportLoadingBar: React.FC<ImportLoadingBarProps> = ({
11+
taskId,
12+
splitgraphNamespace,
13+
splitgraphRepository,
14+
}) => {
15+
const [{ stepperState }, dispatch] = useStepper();
16+
17+
useEffect(() => {
18+
if (!taskId || !splitgraphNamespace || !splitgraphRepository) {
19+
console.log("Don't check import until we have all the right variables");
20+
console.table({
21+
taskId: taskId ?? "no task id",
22+
splitgraphNamespace: splitgraphNamespace ?? "no namespace",
23+
splitgraphRepository: splitgraphRepository ?? "no repo",
24+
});
25+
return;
26+
}
27+
28+
if (stepperState !== "awaiting_import") {
29+
console.log("Done waiting");
30+
return;
31+
}
32+
33+
const checkImportStatus = async () => {
34+
try {
35+
const response = await fetch("/api/await-import-from-github", {
36+
method: "POST",
37+
headers: {
38+
"Content-Type": "application/json",
39+
},
40+
body: JSON.stringify({
41+
taskId,
42+
splitgraphNamespace,
43+
splitgraphRepository,
44+
}),
45+
});
46+
const data = await response.json();
47+
48+
if (data.completed) {
49+
dispatch({ type: "import_complete" });
50+
} else if (data.error) {
51+
dispatch({ type: "import_error", error: data.error });
52+
}
53+
} catch (error) {
54+
console.error("Error occurred during import task status check:", error);
55+
dispatch({
56+
type: "import_error",
57+
error: "An error occurred during the import process",
58+
});
59+
}
60+
};
61+
62+
const interval = setInterval(checkImportStatus, 3000);
63+
64+
return () => clearInterval(interval);
65+
}, [
66+
stepperState,
67+
taskId,
68+
splitgraphNamespace,
69+
splitgraphRepository,
70+
dispatch,
71+
]);
72+
73+
return <div>Loading...</div>;
74+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.importPanel {
2+
background: inherit;
3+
}
4+
5+
.error {
6+
background-color: var(--danger);
7+
padding: 8px;
8+
border: 1px solid var(--sidebar);
9+
margin-bottom: 8px;
10+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useState } from "react";
2+
import { useStepper } from "./StepperContext";
3+
import { ImportLoadingBar } from "./ImportLoadingBar";
4+
5+
import styles from "./ImportPanel.module.css";
6+
7+
export const ImportPanel = () => {
8+
const [
9+
{ stepperState, taskId, error, splitgraphNamespace, splitgraphRepository },
10+
dispatch,
11+
] = useStepper();
12+
const [inputValue, setInputValue] = useState("");
13+
14+
const handleInputSubmit = async (e: React.FormEvent) => {
15+
e.preventDefault();
16+
17+
if (!isValidRepoName(inputValue)) {
18+
dispatch({
19+
type: "import_error",
20+
error:
21+
"Invalid GitHub repository name. Format must be 'namespace/repository'",
22+
});
23+
return;
24+
}
25+
26+
const [githubNamespace, githubRepository] = inputValue.split("/");
27+
28+
try {
29+
const response = await fetch(`/api/start-import-from-github`, {
30+
method: "POST",
31+
headers: {
32+
"Content-Type": "application/json",
33+
},
34+
body: JSON.stringify({ githubSourceRepository: inputValue }),
35+
});
36+
37+
if (!response.ok) {
38+
throw new Error("Network response was not ok");
39+
}
40+
41+
const data = await response.json();
42+
43+
if (!data.taskId) {
44+
throw new Error("Response missing taskId");
45+
}
46+
47+
if (!data.destination || !data.destination.splitgraphNamespace) {
48+
throw new Error("Response missing destination.splitgraphNamespace");
49+
}
50+
51+
if (!data.destination || !data.destination.splitgraphRepository) {
52+
throw new Error("Response missing destination.splitgraphRepository");
53+
}
54+
55+
dispatch({
56+
type: "start_import",
57+
repository: {
58+
namespace: githubNamespace,
59+
repository: githubRepository,
60+
},
61+
taskId: data.taskId,
62+
splitgraphRepository: data.destination.splitgraphRepository as string,
63+
splitgraphNamespace: data.destination.splitgraphNamespace as string,
64+
});
65+
} catch (error) {
66+
dispatch({ type: "import_error", error: error.message });
67+
}
68+
};
69+
70+
const isValidRepoName = (repoName: string) => {
71+
// A valid GitHub repo name should contain exactly one '/'
72+
return /^[\w-.]+\/[\w-.]+$/.test(repoName);
73+
};
74+
75+
return (
76+
<div className={styles.importPanel}>
77+
{stepperState === "unstarted" && (
78+
<>
79+
{error && <p className={styles.error}>{error}</p>}
80+
<form onSubmit={handleInputSubmit}>
81+
<input
82+
type="text"
83+
placeholder="Enter repository name"
84+
value={inputValue}
85+
onChange={(e) => setInputValue(e.target.value)}
86+
/>
87+
<button type="submit">Start Import</button>
88+
</form>
89+
</>
90+
)}
91+
{stepperState === "awaiting_import" && (
92+
<ImportLoadingBar
93+
taskId={taskId}
94+
splitgraphNamespace={splitgraphNamespace}
95+
splitgraphRepository={splitgraphRepository}
96+
/>
97+
)}
98+
{stepperState === "import_complete" && (
99+
<div>
100+
<p>Import Complete</p>
101+
</div>
102+
)}
103+
</div>
104+
);
105+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.stepper {
2+
/* Add styling as necessary */
3+
background: inherit;
4+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { StepperContextProvider } from "./StepperContext";
2+
import { ImportPanel } from "./ImportPanel"; // will create this component later
3+
import { ExportPanel } from "./ExportPanel"; // will create this component later
4+
5+
import styles from "./Stepper.module.css";
6+
7+
export const Stepper = () => {
8+
return (
9+
<StepperContextProvider>
10+
<div className={styles.stepper}>
11+
<ImportPanel />
12+
<ExportPanel />
13+
</div>
14+
</StepperContextProvider>
15+
);
16+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// StepperContext.tsx
2+
import React, { useReducer, useContext, ReactNode } from "react";
3+
import {
4+
StepperState,
5+
StepperAction,
6+
initialState,
7+
stepperReducer,
8+
} from "./stepper-states";
9+
10+
// Define the context
11+
const StepperContext = React.createContext<
12+
[StepperState, React.Dispatch<StepperAction>] | undefined
13+
>(undefined);
14+
15+
export const StepperContextProvider: React.FC<{ children: ReactNode }> = ({
16+
children,
17+
}) => {
18+
const [state, dispatch] = useReducer(stepperReducer, initialState);
19+
20+
return (
21+
<StepperContext.Provider value={[state, dispatch]}>
22+
{children}
23+
</StepperContext.Provider>
24+
);
25+
};
26+
27+
// Custom hook for using the stepper context
28+
export const useStepper = () => {
29+
const context = useContext(StepperContext);
30+
if (!context) {
31+
throw new Error("useStepper must be used within a StepperContextProvider");
32+
}
33+
return context;
34+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// stepper-states.ts
2+
export type GitHubRepository = { namespace: string; repository: string };
3+
4+
// Define the state
5+
export type StepperState = {
6+
stepperState:
7+
| "unstarted"
8+
| "awaiting_import"
9+
| "import_complete"
10+
| "awaiting_export"
11+
| "export_complete";
12+
repository?: GitHubRepository | null;
13+
taskId?: string | null;
14+
error?: string;
15+
tables?: { taskId: string }[] | null;
16+
splitgraphRepository?: string;
17+
splitgraphNamespace?: string;
18+
};
19+
20+
// Define the actions
21+
export type StepperAction =
22+
| {
23+
type: "start_import";
24+
repository: GitHubRepository;
25+
taskId: string;
26+
splitgraphRepository: string;
27+
splitgraphNamespace: string;
28+
}
29+
| { type: "import_complete" }
30+
| { type: "start_export"; tables: { taskId: string }[] }
31+
| { type: "export_complete" }
32+
| { type: "import_error"; error: string }
33+
| { type: "reset" };
34+
35+
// Initial state
36+
export const initialState: StepperState = {
37+
stepperState: "unstarted",
38+
repository: null,
39+
splitgraphRepository: null,
40+
splitgraphNamespace: null,
41+
taskId: null,
42+
tables: null,
43+
};
44+
45+
// Reducer function
46+
export const stepperReducer = (
47+
state: StepperState,
48+
action: StepperAction
49+
): StepperState => {
50+
console.log("Got action", action, "prev state:", state);
51+
switch (action.type) {
52+
case "start_import":
53+
return {
54+
...state,
55+
stepperState: "awaiting_import",
56+
repository: action.repository,
57+
taskId: action.taskId,
58+
splitgraphNamespace: action.splitgraphNamespace,
59+
splitgraphRepository: action.splitgraphRepository,
60+
};
61+
case "import_complete":
62+
return {
63+
...state,
64+
stepperState: "import_complete",
65+
};
66+
case "start_export":
67+
return {
68+
...state,
69+
stepperState: "awaiting_export",
70+
tables: action.tables,
71+
};
72+
case "export_complete":
73+
return {
74+
...state,
75+
stepperState: "export_complete",
76+
};
77+
case "import_error":
78+
return {
79+
...state,
80+
splitgraphRepository: null,
81+
splitgraphNamespace: null,
82+
taskId: null,
83+
stepperState: "unstarted",
84+
error: action.error,
85+
};
86+
87+
case "reset":
88+
return initialState;
89+
90+
default:
91+
return state;
92+
}
93+
};

0 commit comments

Comments
 (0)