Skip to content

Commit 903fcf1

Browse files
authored
[Connector Builder] Display server errors and handle empty streams list (#19461)
* save error handling progress * display error from stream read * rearrange components in stream testing panel * fix state/requests/error handling and properly handle empty streams list * remove commented code, unused components, and console logs * use unknown error in the case of empty error message * don't override numberbadge style * move 'No streams detected' into en.json * handle list streams error better * add loading states for yaml editor and side panel * add state service changes to support loading states * fix manifest template * fix promise * fix api override * undo change to apiOverride
1 parent acea2a5 commit 903fcf1

21 files changed

+392
-242
lines changed

airbyte-webapp/src/components/StreamTestingPanel/LogsDisplay.module.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@
2222
.logsDisplay {
2323
overflow-y: auto;
2424
height: 100%;
25+
padding: variables.$spacing-md;
26+
}
27+
28+
.error {
29+
color: colors.$red;
2530
}

airbyte-webapp/src/components/StreamTestingPanel/LogsDisplay.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import { formatJson } from "./utils";
1111

1212
interface LogsDisplayProps {
1313
logs: StreamReadLogsItem[];
14+
error?: string;
1415
onTitleClick: () => void;
1516
}
1617

17-
export const LogsDisplay: React.FC<LogsDisplayProps> = ({ logs, onTitleClick }) => {
18+
export const LogsDisplay: React.FC<LogsDisplayProps> = ({ logs, error, onTitleClick }) => {
1819
const formattedLogs = useMemo(() => formatJson(logs), [logs]);
1920

2021
return (
@@ -23,10 +24,10 @@ export const LogsDisplay: React.FC<LogsDisplayProps> = ({ logs, onTitleClick })
2324
<Text size="sm" bold>
2425
<FormattedMessage id="connectorBuilder.connectorLogs" />
2526
</Text>
26-
<NumberBadge value={logs.length} color="blue" />
27+
{error !== undefined && <NumberBadge value={1} color="red" />}
2728
</button>
2829
<div className={styles.logsDisplay}>
29-
<pre>{formattedLogs}</pre>
30+
{error !== undefined ? <Text className={styles.error}>{error}</Text> : <pre>{formattedLogs}</pre>}
3031
</div>
3132
</div>
3233
);

airbyte-webapp/src/components/StreamTestingPanel/ResultDisplay.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
margin-top: auto;
3131
margin-bottom: 0;
3232
flex: 0 0 auto;
33+
background-color: colors.$white;
3334
}
3435

3536
.pageLabel {

airbyte-webapp/src/components/StreamTestingPanel/ResultDisplay.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Paginator } from "components/ui/Paginator";
44
import { Text } from "components/ui/Text";
55

66
import { StreamReadSlicesItem } from "core/request/ConnectorBuilderClient";
7-
import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService";
7+
import { useSelectedPageAndSlice } from "services/connectorBuilder/ConnectorBuilderStateService";
88

99
import { PageDisplay } from "./PageDisplay";
1010
import styles from "./ResultDisplay.module.scss";
@@ -16,7 +16,7 @@ interface ResultDisplayProps {
1616
}
1717

1818
export const ResultDisplay: React.FC<ResultDisplayProps> = ({ slices, className }) => {
19-
const { selectedSlice, selectedPage, setSelectedSlice, setSelectedPage } = useConnectorBuilderState();
19+
const { selectedSlice, selectedPage, setSelectedSlice, setSelectedPage } = useSelectedPageAndSlice();
2020

2121
const slice = slices[selectedSlice];
2222
const numPages = slice.pages.length;

airbyte-webapp/src/components/StreamTestingPanel/StreamSelector.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
.centered {
55
margin-left: auto;
66
margin-right: auto;
7-
width: 90%;
7+
width: 75%;
88
max-width: 320px;
99
}
1010

airbyte-webapp/src/components/StreamTestingPanel/StreamSelector.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import { capitalize } from "lodash";
66
import { Heading } from "components/ui/Heading";
77
import { ListBox, ListBoxControlButtonProps } from "components/ui/ListBox";
88

9+
import { StreamsListReadStreamsItem } from "core/request/ConnectorBuilderClient";
910
import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService";
1011

1112
import styles from "./StreamSelector.module.scss";
1213

1314
interface StreamSelectorProps {
1415
className?: string;
16+
streams: StreamsListReadStreamsItem[];
17+
selectedStream: StreamsListReadStreamsItem;
1518
}
1619

1720
const ControlButton: React.FC<ListBoxControlButtonProps<string>> = ({ selectedOption }) => {
@@ -25,8 +28,8 @@ const ControlButton: React.FC<ListBoxControlButtonProps<string>> = ({ selectedOp
2528
);
2629
};
2730

28-
export const StreamSelector: React.FC<StreamSelectorProps> = ({ className }) => {
29-
const { streams, selectedStream, setSelectedStream } = useConnectorBuilderState();
31+
export const StreamSelector: React.FC<StreamSelectorProps> = ({ className, streams, selectedStream }) => {
32+
const { setSelectedStream } = useConnectorBuilderState();
3033
const options = streams.map((stream) => {
3134
return { label: capitalize(stream.name), value: stream.name };
3235
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
@use "scss/variables";
2+
@use "scss/colors";
3+
4+
.container {
5+
flex: 1;
6+
display: flex;
7+
flex-direction: column;
8+
align-items: center;
9+
gap: variables.$spacing-lg;
10+
}
11+
12+
.resizablePanelsContainer {
13+
flex: 1;
14+
min-height: 0;
15+
16+
// required to hide the connector logs splitter underneath the resizable panel overlay
17+
z-index: 0;
18+
}
19+
20+
.testButton {
21+
width: 100%;
22+
}
23+
24+
.testButtonTooltipContainer {
25+
width: 100%;
26+
}
27+
28+
.testButtonText {
29+
color: colors.$white;
30+
}
31+
32+
.url {
33+
color: colors.$blue;
34+
font-weight: 400;
35+
}
36+
37+
:export {
38+
testIconHeight: 17px;
39+
}
40+
41+
.fetchingSpinner {
42+
margin: auto;
43+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { faWarning } from "@fortawesome/free-solid-svg-icons";
2+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3+
import { useEffect, useState } from "react";
4+
import { FormattedMessage, useIntl } from "react-intl";
5+
6+
import { RotateIcon } from "components/icons/RotateIcon";
7+
import { Button } from "components/ui/Button";
8+
import { ResizablePanels } from "components/ui/ResizablePanels";
9+
import { Spinner } from "components/ui/Spinner";
10+
import { Text } from "components/ui/Text";
11+
import { Tooltip } from "components/ui/Tooltip";
12+
13+
import { StreamsListReadStreamsItem } from "core/request/ConnectorBuilderClient";
14+
import { useReadStream } from "services/connectorBuilder/ConnectorBuilderApiService";
15+
import { useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService";
16+
17+
import { LogsDisplay } from "./LogsDisplay";
18+
import { ResultDisplay } from "./ResultDisplay";
19+
import styles from "./StreamTester.module.scss";
20+
21+
interface StreamTesterProps {
22+
selectedStream: StreamsListReadStreamsItem;
23+
}
24+
25+
export const StreamTester: React.FC<StreamTesterProps> = ({ selectedStream }) => {
26+
const { formatMessage } = useIntl();
27+
const { jsonManifest, configJson, yamlIsValid } = useConnectorBuilderState();
28+
const {
29+
data: streamReadData,
30+
refetch: readStream,
31+
isError,
32+
error,
33+
isFetching,
34+
} = useReadStream({
35+
manifest: jsonManifest,
36+
stream: selectedStream.name,
37+
config: configJson,
38+
});
39+
40+
const [logsFlex, setLogsFlex] = useState(0);
41+
const handleLogsTitleClick = () => {
42+
// expand to 50% if it is currently minimized, otherwise minimize it
43+
setLogsFlex((prevFlex) => (prevFlex < 0.06 ? 0.5 : 0));
44+
};
45+
46+
const unknownErrorMessage = formatMessage({ id: "connectorBuilder.unknownError" });
47+
const errorMessage = isError
48+
? error instanceof Error
49+
? error.message || unknownErrorMessage
50+
: unknownErrorMessage
51+
: undefined;
52+
53+
useEffect(() => {
54+
if (isError) {
55+
setLogsFlex(1);
56+
} else {
57+
setLogsFlex(0);
58+
}
59+
}, [isError]);
60+
61+
const testButton = (
62+
<Button
63+
className={styles.testButton}
64+
size="sm"
65+
onClick={() => {
66+
readStream();
67+
}}
68+
disabled={!yamlIsValid}
69+
icon={
70+
yamlIsValid ? (
71+
<div>
72+
<RotateIcon width={styles.testIconHeight} height={styles.testIconHeight} />
73+
</div>
74+
) : (
75+
<FontAwesomeIcon icon={faWarning} />
76+
)
77+
}
78+
>
79+
<Text className={styles.testButtonText} size="sm" bold>
80+
<FormattedMessage id="connectorBuilder.testButton" />
81+
</Text>
82+
</Button>
83+
);
84+
85+
return (
86+
<div className={styles.container}>
87+
<Text className={styles.url} size="lg">
88+
{selectedStream.url}
89+
</Text>
90+
{yamlIsValid ? (
91+
testButton
92+
) : (
93+
<Tooltip control={testButton} containerClassName={styles.testButtonTooltipContainer}>
94+
<FormattedMessage id="connectorBuilder.invalidYamlTest" />
95+
</Tooltip>
96+
)}
97+
{isFetching && (
98+
<div className={styles.fetchingSpinner}>
99+
<Spinner />
100+
</div>
101+
)}
102+
{!isFetching && (streamReadData !== undefined || errorMessage !== undefined) && (
103+
<ResizablePanels
104+
className={styles.resizablePanelsContainer}
105+
orientation="horizontal"
106+
firstPanel={{
107+
children: (
108+
<>{streamReadData !== undefined && !isError && <ResultDisplay slices={streamReadData.slices} />}</>
109+
),
110+
minWidth: 80,
111+
}}
112+
secondPanel={{
113+
className: styles.logsContainer,
114+
children: (
115+
<LogsDisplay logs={streamReadData?.logs ?? []} error={errorMessage} onTitleClick={handleLogsTitleClick} />
116+
),
117+
minWidth: 30,
118+
flex: logsFlex,
119+
onStopResize: (newFlex) => {
120+
if (newFlex) {
121+
setLogsFlex(newFlex);
122+
}
123+
},
124+
}}
125+
/>
126+
)}
127+
</div>
128+
);
129+
};
Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,50 @@
11
@use "scss/variables";
2+
@use "scss/colors";
3+
4+
$buttonHeight: 36px;
25

36
.container {
4-
padding: variables.$spacing-xl variables.$spacing-md variables.$spacing-md variables.$spacing-md;
7+
padding: variables.$spacing-lg;
8+
height: 100%;
9+
position: relative;
510
display: flex;
611
flex-direction: column;
7-
height: 100%;
8-
gap: variables.$spacing-lg;
912
}
1013

11-
.streamSelector {
12-
flex: 0 0 auto;
14+
.configButton {
15+
position: absolute;
16+
top: variables.$spacing-lg;
17+
left: variables.$spacing-lg;
18+
height: $buttonHeight;
19+
width: $buttonHeight;
1320
}
1421

15-
.testControls {
22+
.streamSelector {
1623
flex: 0 0 auto;
24+
height: $buttonHeight;
1725
}
1826

19-
.resizablePanelsContainer {
20-
flex: 1;
21-
min-height: 0;
27+
.selectAndTestContainer {
28+
height: 100%;
29+
display: flex;
30+
flex-direction: column;
31+
gap: variables.$spacing-lg;
32+
}
2233

23-
// required to hide the connector logs splitter underneath the resizable panel overlay
24-
z-index: 0;
34+
.listErrorDisplay {
35+
margin: calc(variables.$spacing-lg + $buttonHeight) auto auto;
36+
padding: variables.$spacing-lg;
37+
display: flex;
38+
flex-direction: column;
39+
gap: variables.$spacing-md;
40+
background-color: colors.$blue-50;
41+
border-radius: variables.$border-radius-sm;
2542
}
2643

27-
.placeholder {
28-
margin-left: auto;
29-
margin-right: auto;
44+
.loadingSpinner {
45+
height: 100%;
46+
width: 100%;
47+
display: flex;
48+
align-items: center;
49+
justify-content: center;
3050
}

0 commit comments

Comments
 (0)