Skip to content

Commit 56af26c

Browse files
authored
Add confirmation modal when navigating from modified source or destination settings pages (#12186)
Add routes to destinations and source settings pages Ensure ServiceForm is reset with updated values on successful save
1 parent 8cf4569 commit 56af26c

File tree

8 files changed

+108
-76
lines changed

8 files changed

+108
-76
lines changed

airbyte-webapp/src/components/ConnectorBlocks/ItemTabs.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { FormattedMessage } from "react-intl";
44
import StepsMenu from "components/StepsMenu";
55

66
export enum StepsTypes {
7-
OVERVIEW = "Overview",
8-
SETTINGS = "Settings",
7+
OVERVIEW = "overview",
8+
SETTINGS = "settings",
99
}
1010

11-
type IProps = {
11+
interface IProps {
1212
currentStep: string;
1313
setCurrentStep: (step: string) => void;
14-
};
14+
}
1515

1616
const steps = [
1717
{

airbyte-webapp/src/pages/DestinationPage/DestinationPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const DestinationsPage: React.FC = () => {
1616
<Route path={RoutePaths.DestinationNew} element={<CreateDestinationPage />} />
1717
<Route path={RoutePaths.ConnectionNew} element={<CreationFormPage />} />
1818
<Route
19-
path=":id"
19+
path=":id/*"
2020
element={
2121
<ResourceNotFoundErrorBoundary errorComponent={<StartOverErrorView />}>
2222
<DestinationItemPage />

airbyte-webapp/src/pages/DestinationPage/pages/DestinationItemPage/DestinationItemPage.tsx

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React, { Suspense, useMemo, useState } from "react";
1+
import React, { Suspense, useMemo } from "react";
22
import { FormattedMessage } from "react-intl";
3+
import { Route, Routes } from "react-router-dom";
34

45
import Placeholder, { ResourceTypes } from "components/Placeholder";
56
import Breadcrumbs from "components/Breadcrumbs";
@@ -21,9 +22,8 @@ import DestinationSettings from "./components/DestinationSettings";
2122
import DestinationConnectionTable from "./components/DestinationConnectionTable";
2223

2324
const DestinationItemPage: React.FC = () => {
24-
const { params, push } = useRouter<unknown, { id: string }>();
25-
const [currentStep, setCurrentStep] = useState<string>(StepsTypes.OVERVIEW);
26-
const onSelectStep = (id: string) => setCurrentStep(id);
25+
const { params, push } = useRouter<unknown, { id: string; "*": string }>();
26+
const currentStep = useMemo<string>(() => (params["*"] === "" ? StepsTypes.OVERVIEW : params["*"]), [params]);
2727

2828
const { sources } = useSourceList();
2929

@@ -37,6 +37,11 @@ const DestinationItemPage: React.FC = () => {
3737

3838
const onClickBack = () => push("..");
3939

40+
const onSelectStep = (id: string) => {
41+
const path = id === StepsTypes.OVERVIEW ? "/" : `/${id.toLowerCase()}`;
42+
push(`/destination/${destination.destinationId}${path}`);
43+
};
44+
4045
const breadcrumbsData = [
4146
{
4247
name: <FormattedMessage id="admin.destinations" />,
@@ -75,33 +80,6 @@ const DestinationItemPage: React.FC = () => {
7580
push(path, { state });
7681
};
7782

78-
const renderContent = () => {
79-
if (currentStep === StepsTypes.SETTINGS) {
80-
return (
81-
<DestinationSettings currentDestination={destination} connectionsWithDestination={connectionsWithDestination} />
82-
);
83-
}
84-
85-
return (
86-
<>
87-
<TableItemTitle
88-
type="source"
89-
dropDownData={sourcesDropDownData}
90-
onSelect={onSelect}
91-
entityName={destination.name}
92-
entity={destination.destinationName}
93-
entityIcon={destinationDefinition.icon ? getIcon(destinationDefinition.icon) : null}
94-
releaseStage={destinationDefinition.releaseStage}
95-
/>
96-
{connectionsWithDestination.length ? (
97-
<DestinationConnectionTable connections={connectionsWithDestination} />
98-
) : (
99-
<Placeholder resource={ResourceTypes.Sources} />
100-
)}
101-
</>
102-
);
103-
};
104-
10583
return (
10684
<MainPageWithScroll
10785
headTitle={<HeadTitle titles={[{ id: "admin.destinations" }, { title: destination.name }]} />}
@@ -113,7 +91,40 @@ const DestinationItemPage: React.FC = () => {
11391
/>
11492
}
11593
>
116-
<Suspense fallback={<LoadingPage />}>{renderContent()}</Suspense>
94+
<Suspense fallback={<LoadingPage />}>
95+
<Routes>
96+
<Route
97+
path="/settings"
98+
element={
99+
<DestinationSettings
100+
currentDestination={destination}
101+
connectionsWithDestination={connectionsWithDestination}
102+
/>
103+
}
104+
/>
105+
<Route
106+
index
107+
element={
108+
<>
109+
<TableItemTitle
110+
type="source"
111+
dropDownData={sourcesDropDownData}
112+
onSelect={onSelect}
113+
entityName={destination.name}
114+
entity={destination.destinationName}
115+
entityIcon={destinationDefinition.icon ? getIcon(destinationDefinition.icon) : null}
116+
releaseStage={destinationDefinition.releaseStage}
117+
/>
118+
{connectionsWithDestination.length ? (
119+
<DestinationConnectionTable connections={connectionsWithDestination} />
120+
) : (
121+
<Placeholder resource={ResourceTypes.Sources} />
122+
)}
123+
</>
124+
}
125+
/>
126+
</Routes>
127+
</Suspense>
117128
</MainPageWithScroll>
118129
);
119130
};

airbyte-webapp/src/pages/DestinationPage/pages/DestinationItemPage/components/DestinationConnectionTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const DestinationConnectionTable: React.FC<IProps> = ({ connections }) => {
4545
[connections, syncManualConnection]
4646
);
4747

48-
const clickRow = (source: ITableDataItem) => push(`../../${RoutePaths.Connections}/${source.connectionId}`);
48+
const clickRow = (source: ITableDataItem) => push(`../../../${RoutePaths.Connections}/${source.connectionId}`);
4949

5050
return (
5151
<ConnectionTable

airbyte-webapp/src/pages/SourcesPage/SourcesPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const SourcesPage: React.FC = () => (
1515
<Route path={RoutePaths.SourceNew} element={<CreateSourcePage />} />
1616
<Route path={RoutePaths.ConnectionNew} element={<CreationFormPage />} />
1717
<Route
18-
path=":id"
18+
path=":id/*"
1919
element={
2020
<ResourceNotFoundErrorBoundary errorComponent={<StartOverErrorView />}>
2121
<SourceItemPage />

airbyte-webapp/src/pages/SourcesPage/pages/SourceItemPage/SourceItemPage.tsx

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React, { Suspense, useMemo, useState } from "react";
1+
import React, { Suspense, useMemo } from "react";
22
import { FormattedMessage } from "react-intl";
3+
import { Route, Routes } from "react-router-dom";
34

45
import { DropDownRow } from "components";
56
import PageTitle from "components/PageTitle";
@@ -24,9 +25,8 @@ import SourceSettings from "./components/SourceSettings";
2425
import SourceConnectionTable from "./components/SourceConnectionTable";
2526

2627
const SourceItemPage: React.FC = () => {
27-
const { query, push } = useRouter<{ id: string }>();
28-
const [currentStep, setCurrentStep] = useState<string>(StepsTypes.OVERVIEW);
29-
const onSelectStep = (id: string) => setCurrentStep(id);
28+
const { query, params, push } = useRouter<{ id: string }, { id: string; "*": string }>();
29+
const currentStep = useMemo<string>(() => (params["*"] === "" ? StepsTypes.OVERVIEW : params["*"]), [params]);
3030

3131
const { destinations } = useDestinationList();
3232

@@ -62,6 +62,11 @@ const SourceItemPage: React.FC = () => {
6262
[destinations, destinationDefinitions]
6363
);
6464

65+
const onSelectStep = (id: string) => {
66+
const path = id === StepsTypes.OVERVIEW ? "/" : `/${id.toLowerCase()}`;
67+
push(`/source/${source.sourceId}${path}`);
68+
};
69+
6570
const onSelect = (data: DropDownRow.IDataItem) => {
6671
const path = `../${RoutePaths.ConnectionNew}`;
6772
const state =
@@ -75,31 +80,6 @@ const SourceItemPage: React.FC = () => {
7580
push(path, { state });
7681
};
7782

78-
const renderContent = () => {
79-
if (currentStep === StepsTypes.SETTINGS) {
80-
return <SourceSettings currentSource={source} connectionsWithSource={connectionsWithSource} />;
81-
}
82-
83-
return (
84-
<>
85-
<TableItemTitle
86-
type="destination"
87-
dropDownData={destinationsDropDownData}
88-
onSelect={onSelect}
89-
entity={source.sourceName}
90-
entityName={source.name}
91-
entityIcon={sourceDefinition ? getIcon(sourceDefinition.icon) : null}
92-
releaseStage={sourceDefinition.releaseStage}
93-
/>
94-
{connectionsWithSource.length ? (
95-
<SourceConnectionTable connections={connectionsWithSource} />
96-
) : (
97-
<Placeholder resource={ResourceTypes.Destinations} />
98-
)}
99-
</>
100-
);
101-
};
102-
10383
return (
10484
<MainPageWithScroll
10585
headTitle={<HeadTitle titles={[{ id: "admin.sources" }, { title: source.name }]} />}
@@ -111,7 +91,35 @@ const SourceItemPage: React.FC = () => {
11191
/>
11292
}
11393
>
114-
<Suspense fallback={<LoadingPage />}>{renderContent()}</Suspense>
94+
<Suspense fallback={<LoadingPage />}>
95+
<Routes>
96+
<Route
97+
path="/settings"
98+
element={<SourceSettings currentSource={source} connectionsWithSource={connectionsWithSource} />}
99+
/>
100+
<Route
101+
index
102+
element={
103+
<>
104+
<TableItemTitle
105+
type="destination"
106+
dropDownData={destinationsDropDownData}
107+
onSelect={onSelect}
108+
entity={source.sourceName}
109+
entityName={source.name}
110+
entityIcon={sourceDefinition ? getIcon(sourceDefinition.icon) : null}
111+
releaseStage={sourceDefinition.releaseStage}
112+
/>
113+
{connectionsWithSource.length ? (
114+
<SourceConnectionTable connections={connectionsWithSource} />
115+
) : (
116+
<Placeholder resource={ResourceTypes.Destinations} />
117+
)}
118+
</>
119+
}
120+
></Route>
121+
</Routes>
122+
</Suspense>
115123
</MainPageWithScroll>
116124
);
117125
};

airbyte-webapp/src/pages/SourcesPage/pages/SourceItemPage/components/SourceConnectionTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const SourceConnectionTable: React.FC<IProps> = ({ connections }) => {
4646
[connections, syncManualConnection]
4747
);
4848

49-
const clickRow = (source: ITableDataItem) => push(`../../${RoutePaths.Connections}/${source.connectionId}`);
49+
const clickRow = (source: ITableDataItem) => push(`../../../${RoutePaths.Connections}/${source.connectionId}`);
5050

5151
return (
5252
<ConnectionTable

airbyte-webapp/src/views/Connector/ServiceForm/ServiceForm.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import { Formik, getIn, setIn, useFormikContext } from "formik";
33
import { JSONSchema7 } from "json-schema";
44
import { useToggle } from "react-use";
55

6+
import { FormChangeTracker } from "components/FormChangeTracker";
7+
68
import RequestConnectorModal from "views/Connector/RequestConnectorModal";
79
import { FormBaseItem } from "core/form/types";
810
import { ConnectorDefinition, ConnectorDefinitionSpecification, Scheduler } from "core/domain/connector";
911
import { isDefined } from "utils/common";
12+
import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker";
1013

1114
import { ConnectionConfiguration } from "../../../core/domain/connection";
1215
import {
@@ -36,7 +39,7 @@ const PatchInitialValuesWithWidgetConfig: React.FC<{
3639
schema: JSONSchema7;
3740
}> = ({ schema }) => {
3841
const { widgetsInfo } = useServiceForm();
39-
const { values, setFieldValue } = useFormikContext<ServiceFormValues>();
42+
const { values, resetForm } = useFormikContext<ServiceFormValues>();
4043

4144
const valueRef = useRef<ConnectionConfiguration>();
4245
valueRef.current = values.connectionConfiguration;
@@ -53,7 +56,12 @@ const PatchInitialValuesWithWidgetConfig: React.FC<{
5356
.filter(([k, v]) => isDefined(v.default) && !isDefined(getIn(constPatchedValues, k)))
5457
.reduce((acc, [k, v]) => setIn(acc, k, v.default), constPatchedValues);
5558

56-
setFieldValue("connectionConfiguration", defaultPatchedValues);
59+
resetForm({
60+
values: {
61+
...values,
62+
connectionConfiguration: defaultPatchedValues,
63+
},
64+
});
5765

5866
// eslint-disable-next-line react-hooks/exhaustive-deps
5967
}, [schema]);
@@ -99,6 +107,9 @@ export type ServiceFormProps = {
99107
};
100108

101109
const ServiceForm: React.FC<ServiceFormProps> = (props) => {
110+
const formId = useUniqueFormId();
111+
const { clearFormChange } = useFormChangeTrackerService();
112+
102113
const [isOpenRequestModal, toggleOpenRequestModal] = useToggle(false);
103114
const [initialRequestName, setInitialRequestName] = useState<string>();
104115
const {
@@ -178,12 +189,13 @@ const ServiceForm: React.FC<ServiceFormProps> = (props) => {
178189
);
179190

180191
const onFormSubmit = useCallback(
181-
async (values) => {
192+
async (values: ServiceFormValues) => {
182193
const valuesToSend = getValues(values);
194+
await onSubmit(valuesToSend);
183195

184-
return onSubmit(valuesToSend);
196+
clearFormChange(formId);
185197
},
186-
[getValues, onSubmit]
198+
[clearFormChange, formId, getValues, onSubmit]
187199
);
188200

189201
return (
@@ -194,7 +206,7 @@ const ServiceForm: React.FC<ServiceFormProps> = (props) => {
194206
validationSchema={validationSchema}
195207
onSubmit={onFormSubmit}
196208
>
197-
{() => (
209+
{({ dirty }) => (
198210
<ServiceFormContextProvider
199211
widgetsInfo={uiWidgetsInfo}
200212
getValues={getValues}
@@ -207,6 +219,7 @@ const ServiceForm: React.FC<ServiceFormProps> = (props) => {
207219
>
208220
{!props.isEditMode && <SetDefaultName />}
209221
<FormikPatch />
222+
<FormChangeTracker changed={dirty} formId={formId} />
210223
<PatchInitialValuesWithWidgetConfig schema={jsonSchema} />
211224
<FormRoot
212225
{...props}

0 commit comments

Comments
 (0)