Skip to content

Commit d1d3b62

Browse files
Implement the components for the "Export Panel"
The ExportPanel first renders a "Start Export" button. Then, while the export is running, it renders an `ExportTableLoadingBar` for each table that is being exported. Each of thee individual components sends its own polling request with its `taskId` to the `await-export-to-seafowl-task` endpoint, and upon completion of each task, sends an action to the reducer, which handles it by updating the set of loading tasks. When the set of loading tasks is complete, it changes the `stepperState` to `export_complete`. If any of the tasks has an error, then the `stepperState` changes to `export_error` which should cause all loading bars to unmount - i.e., any error will short-circuit all of the table loading, even if some were to complete. At that point the user can click "start export" again. This completes the logic necessary for import and export, and now it's just a matter of styling the components, linking to the Splitgraph Console, adding explanatory text, and finally rendering a chart with the data. We'll also want to create a meta-repository in Splitgraph for tracking which GitHub repos we've imported so far, analogously to how we track Socrata metadata for each Socrata repo.
1 parent 9c596ef commit d1d3b62

9 files changed

Lines changed: 277 additions & 29 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.exportLoadingBars {
2+
background-color: inherit;
3+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useStepper } from "./StepperContext";
2+
import { ExportTableLoadingBar } from "./ExportTableLoadingBar";
3+
import styles from "./ExportLoadingBars.module.css";
4+
5+
export const ExportLoadingBars = () => {
6+
const [{ exportedTablesLoading }] = useStepper();
7+
8+
return (
9+
<div className={styles.exportLoadingBars}>
10+
{Array.from(exportedTablesLoading).map(({ tableName, taskId }) => (
11+
<ExportTableLoadingBar
12+
key={taskId}
13+
tableName={tableName}
14+
taskId={taskId}
15+
/>
16+
))}
17+
</div>
18+
);
19+
};
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1-
/* ExportPanel.module.css */
2-
31
.exportPanel {
4-
/* Style for export panel will go here */
2+
/* Styles for the export panel container */
3+
background: inherit;
4+
}
5+
6+
.startExportButton {
7+
/* Styles for the start export button */
8+
background: inherit;
9+
}
10+
11+
.querySeafowlButton {
12+
/* Styles for the query Seafowl button */
13+
background: inherit;
14+
}
15+
16+
.viewReportButton {
17+
/* Styles for the view report button */
518
background: inherit;
619
}
Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,77 @@
1-
import styles from "./ExportPanel.module.css";
1+
// components/ImportExportStepper/ExportPanel.tsx
2+
23
import { useStepper } from "./StepperContext";
4+
import styles from "./ExportPanel.module.css";
5+
import { ExportLoadingBars } from "./ExportLoadingBars";
6+
7+
// TODO: don't hardcode this? or at least hardcode all of them and make it official
8+
const importedTableNames = [
9+
"stargazers",
10+
// NOTE: If we only specify stargazers, then stargazers_user is still included since it's a dependent table
11+
"stargazers_user",
12+
];
313

414
export const ExportPanel = () => {
5-
const [{ stepperState }] = useStepper();
15+
const [
16+
{ stepperState, exportError, splitgraphRepository, splitgraphNamespace },
17+
dispatch,
18+
] = useStepper();
19+
20+
const handleStartExport = async () => {
21+
try {
22+
const response = await fetch("/api/start-export-to-seafowl", {
23+
method: "POST",
24+
body: JSON.stringify({
25+
tables: importedTableNames.map((tableName) => ({
26+
namespace: splitgraphNamespace,
27+
repository: splitgraphRepository,
28+
table: tableName,
29+
})),
30+
}),
31+
headers: {
32+
"Content-Type": "application/json",
33+
},
34+
});
35+
const data = await response.json();
636

7-
const disabled =
8-
stepperState !== "import_complete" &&
9-
stepperState !== "awaiting_export" &&
10-
stepperState !== "export_complete";
37+
if (!data.tables || !data.tables.length) {
38+
throw new Error("Response missing tables");
39+
}
1140

12-
// We will fill this in later
41+
dispatch({
42+
type: "start_export",
43+
tables: data.tables.map(
44+
({ tableName, taskId }: { tableName: string; taskId: string }) => ({
45+
taskId,
46+
tableName,
47+
})
48+
),
49+
});
50+
} catch (error) {
51+
dispatch({ type: "export_error", error: error.message });
52+
}
53+
};
1354

1455
return (
1556
<div className={styles.exportPanel}>
16-
{disabled ? "Export disabled" : "Export..."}
57+
{exportError && <p className={styles.error}>{exportError}</p>}
58+
{stepperState === "import_complete" && (
59+
<button
60+
className={styles.startExportButton}
61+
onClick={handleStartExport}
62+
>
63+
Start Export
64+
</button>
65+
)}
66+
{stepperState === "awaiting_export" && <ExportLoadingBars />}
67+
{stepperState === "export_complete" && (
68+
<>
69+
<button className={styles.querySeafowlButton}>
70+
Query Seafowl in Splitgraph Console
71+
</button>
72+
<button className={styles.viewReportButton}>View Report</button>
73+
</>
74+
)}
1775
</div>
1876
);
1977
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* components/ImportExportStepper/ExportTableLoadingBar.module.css */
2+
3+
.exportTableLoadingBar {
4+
background-color: inherit;
5+
}
6+
7+
.loadingBar {
8+
background-color: inherit;
9+
}
10+
11+
.completedBar {
12+
background-color: inherit;
13+
}
14+
15+
.tableName {
16+
background-color: inherit;
17+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useEffect } from "react";
2+
import { useStepper } from "./StepperContext";
3+
import styles from "./ExportTableLoadingBar.module.css";
4+
5+
interface ExportTableLoadingBarProps {
6+
tableName: string;
7+
taskId: string;
8+
}
9+
10+
export const ExportTableLoadingBar = ({
11+
tableName,
12+
taskId,
13+
}: React.PropsWithoutRef<ExportTableLoadingBarProps>) => {
14+
const [{ stepperState, exportedTablesLoading }, dispatch] = useStepper();
15+
16+
useEffect(() => {
17+
if (!taskId || !tableName) {
18+
console.log("Don't check export until we have taskId and tableName");
19+
console.table({
20+
taskId,
21+
tableName,
22+
});
23+
return;
24+
}
25+
26+
if (stepperState !== "awaiting_export") {
27+
console.log("Done waiting for export");
28+
return;
29+
}
30+
31+
const pollExportTask = async () => {
32+
try {
33+
const response = await fetch("/api/await-export-to-seafowl-task", {
34+
method: "POST",
35+
headers: {
36+
"Content-Type": "application/json",
37+
},
38+
body: JSON.stringify({
39+
taskId,
40+
}),
41+
});
42+
const data = await response.json();
43+
44+
if (data.completed) {
45+
dispatch({
46+
type: "export_table_task_complete",
47+
completedTable: { tableName, taskId },
48+
});
49+
} else if (data.error) {
50+
throw new Error(data.error);
51+
}
52+
} catch (error) {
53+
dispatch({
54+
type: "export_error",
55+
error: `Error exporting ${tableName}: ${error.message}`,
56+
});
57+
}
58+
};
59+
60+
const interval = setInterval(pollExportTask, 3000);
61+
return () => clearInterval(interval);
62+
}, [stepperState, tableName, taskId, dispatch]);
63+
64+
const isLoading = !!Array.from(exportedTablesLoading).find(
65+
(t) => t.taskId === taskId
66+
);
67+
68+
return (
69+
<div className={styles.exportTableLoadingBar}>
70+
<div className={styles.loadingBar}>
71+
{isLoading
72+
? `Loading ${tableName}...`
73+
: `Successfully exported ${tableName}`}
74+
</div>
75+
<div className={styles.tableName}>{tableName}</div>
76+
</div>
77+
);
78+
};

examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ImportLoadingBar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export const ImportLoadingBar: React.FC<ImportLoadingBarProps> = ({
5151
dispatch({ type: "import_error", error: data.error });
5252
}
5353
} catch (error) {
54-
console.error("Error occurred during import task status check:", error);
5554
dispatch({
5655
type: "import_error",
5756
error: "An error occurred during the import process",

examples/nextjs-import-airbyte-github-export-seafowl/components/ImportExportStepper/ImportPanel.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import styles from "./ImportPanel.module.css";
66

77
export const ImportPanel = () => {
88
const [
9-
{ stepperState, taskId, error, splitgraphNamespace, splitgraphRepository },
9+
{
10+
stepperState,
11+
importTaskId,
12+
importError,
13+
splitgraphNamespace,
14+
splitgraphRepository,
15+
},
1016
dispatch,
1117
] = useStepper();
1218
const [inputValue, setInputValue] = useState("");
@@ -76,7 +82,7 @@ export const ImportPanel = () => {
7682
<div className={styles.importPanel}>
7783
{stepperState === "unstarted" && (
7884
<>
79-
{error && <p className={styles.error}>{error}</p>}
85+
{importError && <p className={styles.error}>{importError}</p>}
8086
<form onSubmit={handleInputSubmit}>
8187
<input
8288
type="text"
@@ -90,7 +96,7 @@ export const ImportPanel = () => {
9096
)}
9197
{stepperState === "awaiting_import" && (
9298
<ImportLoadingBar
93-
taskId={taskId}
99+
taskId={importTaskId}
94100
splitgraphNamespace={splitgraphNamespace}
95101
splitgraphRepository={splitgraphRepository}
96102
/>

0 commit comments

Comments
 (0)