Skip to content

Commit 8cfad1b

Browse files
teallarsonedmunditoandyjihkrishnaglick
authored
Don't allow users to edit deleted connections (#12254)
* add 'deleted' mode to ConnectionForm Co-authored-by: Edmundo Ruiz Ghanem <[email protected]> * refactor 'out of credits banner' to generic error banner * Disable fields and remove buttons * use mode instead of checking connection, cleanup * close... todo finish replication page (search, refresh button) * cleanup * error banner is really an alert banner * make ConnectionFormMode its own type * Update airbyte-webapp/src/components/MainPageWithScroll/MainPageWithScroll.tsx Co-authored-by: Edmundo Ruiz Ghanem <[email protected]> * Update airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx Co-authored-by: Edmundo Ruiz Ghanem <[email protected]> * cleanup, types * Update airbyte-webapp/src/locales/en.json Co-authored-by: Andy Jih <[email protected]> * fix transformation edit mode, fix circleLoader css error * fix fieldset pointerEvents * Update airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ConnectionPageTitle.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusView.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/StatusView.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/views/Connection/FormCard.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx Co-authored-by: Krishna Glick <[email protected]> * Update airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx Co-authored-by: Krishna Glick <[email protected]> * string union types to use exported type * formik does not like unknown * fix modes, types on transformation forms Co-authored-by: Krishna Glick <[email protected]> * inline export * clean up types/exports for ArrayOfObjectsEditor * EditorHeader tests * connection form tests * types cleanup * import fix * fix another bad import * Don't show checkboxes on replication view page for deleted * use `NavigateReplace` on settings route when deleted * fix test * wording tweak from code review * clarity in EditorHeader test from code review * fix deleted view of connection form fields Co-authored-by: Edmundo Ruiz Ghanem <[email protected]> Co-authored-by: Edmundo Ruiz Ghanem <[email protected]> Co-authored-by: Andy Jih <[email protected]> Co-authored-by: Krishna Glick <[email protected]> Co-authored-by: Krishna Glick <[email protected]>
1 parent 21c1364 commit 8cfad1b

File tree

29 files changed

+511
-222
lines changed

29 files changed

+511
-222
lines changed

airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.tsx

+20-14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import styled from "styled-components";
44

55
import { Button } from "components";
66

7+
import { ConnectionFormMode } from "views/Connection/ConnectionForm/ConnectionForm";
8+
79
import { EditorHeader } from "./components/EditorHeader";
810
import { EditorRow } from "./components/EditorRow";
911

@@ -26,7 +28,7 @@ const Content = styled.div`
2628
margin-bottom: 20px;
2729
`;
2830

29-
type ArrayOfObjectsEditorProps<T extends { name: string }> = {
31+
export interface ArrayOfObjectsEditorProps<T extends { name: string }> {
3032
items: T[];
3133
editableItemIndex?: number | string | null;
3234
children: (item?: T) => React.ReactNode;
@@ -36,20 +38,26 @@ type ArrayOfObjectsEditorProps<T extends { name: string }> = {
3638
onCancelEdit?: () => void;
3739
onDone?: () => void;
3840
onRemove: (index: number) => void;
39-
};
41+
mode?: ConnectionFormMode;
42+
}
4043

41-
function ArrayOfObjectsEditor<T extends { name: string } = { name: string }>(
42-
props: ArrayOfObjectsEditorProps<T>
43-
): JSX.Element {
44-
const { onStartEdit, onDone, onRemove, onCancelEdit, items, editableItemIndex, children, mainTitle, addButtonText } =
45-
props;
44+
export function ArrayOfObjectsEditor<T extends { name: string } = { name: string }>({
45+
onStartEdit,
46+
onDone,
47+
onRemove,
48+
onCancelEdit,
49+
items,
50+
editableItemIndex,
51+
children,
52+
mainTitle,
53+
addButtonText,
54+
mode,
55+
}: ArrayOfObjectsEditorProps<T>): JSX.Element {
4656
const onAddItem = React.useCallback(() => onStartEdit(items.length), [onStartEdit, items]);
4757

48-
const isEditMode = editableItemIndex !== null && editableItemIndex !== undefined;
49-
50-
if (isEditMode) {
58+
const isEditable = editableItemIndex !== null && editableItemIndex !== undefined;
59+
if (mode !== "readonly" && isEditable) {
5160
const item = typeof editableItemIndex === "number" ? items[editableItemIndex] : undefined;
52-
5361
return (
5462
<Content>
5563
{children(item)}
@@ -78,6 +86,7 @@ function ArrayOfObjectsEditor<T extends { name: string } = { name: string }>(
7886
onAddItem={onAddItem}
7987
mainTitle={mainTitle}
8088
addButtonText={addButtonText}
89+
mode={mode}
8190
/>
8291
{items.length ? (
8392
<ItemsList>
@@ -89,6 +98,3 @@ function ArrayOfObjectsEditor<T extends { name: string } = { name: string }>(
8998
</Content>
9099
);
91100
}
92-
93-
export { ArrayOfObjectsEditor };
94-
export type { ArrayOfObjectsEditorProps };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { render } from "utils/testutils";
2+
3+
import { EditorHeader } from "./EditorHeader";
4+
5+
describe("<ArrayOfObjectsEditor />", () => {
6+
let container: HTMLElement;
7+
describe("edit mode", () => {
8+
test("it renders only relevant items for the mode", async () => {
9+
const renderResult = await render(
10+
<EditorHeader
11+
mainTitle={<div data-testid="mainTitle">"This is the main title"</div>}
12+
addButtonText={<div data-testid="addButtonText">"button text"</div>}
13+
itemsCount={0}
14+
onAddItem={() => {
15+
return null;
16+
}}
17+
mode="edit"
18+
/>
19+
);
20+
container = renderResult.container;
21+
const mainTitle = container.querySelector("div[data-testid='mainTitle']");
22+
const addButtonText = container.querySelector("div[data-testid='addButtonText']");
23+
expect(mainTitle).toBeInTheDocument();
24+
expect(addButtonText).toBeInTheDocument();
25+
});
26+
});
27+
describe("readonly mode", () => {
28+
test("it renders only relevant items for the mode", async () => {
29+
const renderResult = await render(
30+
<EditorHeader
31+
mainTitle={<div data-testid="mainTitle">"This is the main title"</div>}
32+
addButtonText={<div data-testid="addButtonText">"button text"</div>}
33+
itemsCount={0}
34+
onAddItem={() => {
35+
return null;
36+
}}
37+
mode="readonly"
38+
/>
39+
);
40+
container = renderResult.container;
41+
const mainTitle = container.querySelector("div[data-testid='mainTitle']");
42+
expect(mainTitle).toBeInTheDocument();
43+
expect(container.querySelector("div[data-testid='addButtonText']")).not.toBeInTheDocument();
44+
});
45+
});
46+
});

airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import styled from "styled-components";
44

55
import { Button } from "components";
66

7+
import { ConnectionFormMode } from "views/Connection/ConnectionForm/ConnectionForm";
8+
79
const Content = styled.div`
810
display: flex;
911
justify-content: space-between;
@@ -21,15 +23,18 @@ type EditorHeaderProps = {
2123
addButtonText?: React.ReactNode;
2224
itemsCount: number;
2325
onAddItem: () => void;
26+
mode?: ConnectionFormMode;
2427
};
2528

26-
const EditorHeader: React.FC<EditorHeaderProps> = ({ itemsCount, onAddItem, mainTitle, addButtonText }) => {
29+
const EditorHeader: React.FC<EditorHeaderProps> = ({ itemsCount, onAddItem, mainTitle, addButtonText, mode }) => {
2730
return (
2831
<Content>
2932
{mainTitle || <FormattedMessage id="form.items" values={{ count: itemsCount }} />}
30-
<Button secondary type="button" onClick={onAddItem} data-testid="addItemButton">
31-
{addButtonText || <FormattedMessage id="form.addItems" />}
32-
</Button>
33+
{mode !== "readonly" && (
34+
<Button secondary type="button" onClick={onAddItem} data-testid="addItemButton">
35+
{addButtonText || <FormattedMessage id="form.addItems" />}
36+
</Button>
37+
)}
3338
</Content>
3439
);
3540
};

airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ const CreateConnectionContent: React.FC<IProps> = ({
117117
) : (
118118
<Suspense fallback={<LoadingSchema />}>
119119
<ConnectionForm
120+
mode="create"
120121
connection={connection}
121122
additionBottomControls={additionBottomControls}
122123
onDropDownSelect={onSelectFrequency}

airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const AdditionMessage = styled.span`
3838

3939
const LabeledRadioButton: React.FC<IProps> = (props) => (
4040
<ControlContainer className={props.className}>
41-
<RadioButton {...props} id={`radiobutton-${props.id || props.name}`} />
41+
<RadioButton {...props} id={`radiobutton-${props.id || props.name}`} disabled={props.disabled} />
4242
<Label disabled={props.disabled} htmlFor={`radiobutton-${props.id || props.name}`}>
4343
{props.label}
4444
<AdditionMessage>{props.message}</AdditionMessage>

airbyte-webapp/src/components/MainPageWithScroll/MainPageWithScroll.tsx

+11-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ const Content = styled.div`
77
height: 100%;
88
`;
99

10+
const Header = styled.div<{ hasError?: boolean }>`
11+
padding-top: ${({ hasError }) => (hasError ? 25 : 0)}px;
12+
`;
13+
1014
const Page = styled.div`
1115
overflow-y: hidden;
1216
height: 100%;
@@ -19,16 +23,20 @@ const Page = styled.div`
1923
* @param pageTitle the title shown on the page
2024
*/
2125
type IProps = {
26+
error?: React.ReactNode;
2227
headTitle?: React.ReactNode;
2328
pageTitle?: React.ReactNode;
2429
children?: React.ReactNode;
2530
};
2631

27-
const MainPageWithScroll: React.FC<IProps> = ({ headTitle, pageTitle, children }) => {
32+
const MainPageWithScroll: React.FC<IProps> = ({ error, headTitle, pageTitle, children }) => {
2833
return (
2934
<Page>
30-
{headTitle}
31-
{pageTitle}
35+
{error}
36+
<Header hasError={!!error}>
37+
{headTitle}
38+
{pageTitle}
39+
</Header>
3240
<Content>{children}</Content>
3341
</Page>
3442
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from "react";
2+
import { FormattedMessage } from "react-intl";
3+
import styled from "styled-components";
4+
5+
import { Link } from "components/Link";
6+
7+
import { CloudRoutes } from "packages/cloud/cloudRoutes";
8+
import { CreditStatus } from "packages/cloud/lib/domain/cloudWorkspaces/types";
9+
10+
const Container = styled.div<{ errorType?: string }>`
11+
height: 30px;
12+
width: 100%;
13+
background: ${({ errorType, theme }) => (errorType === "credits" ? theme.redColor : theme.warningColor)};
14+
color: ${({ theme }) => theme.blackColor};
15+
text-align: center;
16+
position: fixed;
17+
z-index: 3;
18+
font-size: 12px;
19+
line-height: 30px;
20+
`;
21+
const CreditsLink = styled(Link)`
22+
color: ${({ theme }) => theme.blackColor};
23+
`;
24+
25+
interface AlertBannerProps {
26+
alertType: string;
27+
id: CreditStatus | string;
28+
}
29+
30+
export const AlertBanner: React.FC<AlertBannerProps> = ({ alertType: errorType, id }) => (
31+
<Container errorType={errorType}>
32+
{errorType === "credits" ? (
33+
<FormattedMessage
34+
id={id}
35+
values={{ lnk: (content: React.ReactNode) => <CreditsLink to={CloudRoutes.Credits}>{content}</CreditsLink> }}
36+
/>
37+
) : (
38+
<FormattedMessage id={id} />
39+
)}
40+
</Container>
41+
);

airbyte-webapp/src/components/base/RadioButton/RadioButton.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ const Check = styled.div<{ checked?: boolean }>`
1515
background: ${({ theme, checked }) => (checked ? theme.whiteColor : theme.greyColor20)};
1616
`;
1717

18-
const RadioButtonContainer = styled.label<{ checked?: boolean }>`
18+
const RadioButtonContainer = styled.label<{ checked?: boolean; disabled?: boolean }>`
1919
height: 18px;
2020
width: 18px;
2121
background: ${({ theme, checked }) => (checked ? theme.primaryColor : theme.whiteColor)};
2222
border: 1px solid ${({ theme, checked }) => (checked ? theme.primaryColor : theme.greyColor20)};
23+
opacity: ${({ disabled }) => (disabled === true ? 0.5 : 1)};
2324
color: ${({ theme }) => theme.whiteColor};
2425
text-align: center;
2526
border-radius: 50%;
@@ -34,6 +35,7 @@ const RadioButton: React.FC<React.InputHTMLAttributes<HTMLInputElement>> = (prop
3435
className={props.className}
3536
onClick={(event: React.SyntheticEvent) => event.stopPropagation()}
3637
checked={props.checked}
38+
disabled={props.disabled}
3739
>
3840
<Check checked={props.checked} />
3941
<RadioButtonInput {...props} type="radio" />

airbyte-webapp/src/core/domain/connection/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export type ScheduleProperties = {
3333
export enum ConnectionStatus {
3434
ACTIVE = "active",
3535
INACTIVE = "inactive",
36-
DEPRECATED = "depreacted",
36+
DEPRECATED = "deprecated",
3737
}
3838

3939
export interface Connection {

airbyte-webapp/src/locales/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"form.primaryKey": "Primary key",
4747
"form.cursorField": "Cursor field",
4848
"form.dataSync": "Activate the streams you want to sync",
49+
"form.dataSync.readonly": "Activated streams",
4950
"form.dataSync.message": "Don’t worry! You’ll be able to change this later on.",
5051
"form.cancel": "Cancel",
5152
"form.submit": "Submit",
@@ -338,6 +339,7 @@
338339
"connection.transfer": "Transfer",
339340
"connection.linkCopied": "Link copied!",
340341
"connection.copyLogLink": "Copy link to log",
342+
"connection.connectionDeletedView": "This connection has been deleted. You can’t make any changes or run syncs.",
341343

342344
"form.frequency": "Replication frequency*",
343345
"form.frequency.placeholder": "Select a frequency",

airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Outlet } from "react-router-dom";
33
import styled from "styled-components";
44

55
import { LoadingPage } from "components";
6+
import { AlertBanner } from "components/base/Banner/AlertBanner";
67

78
import { CreditStatus } from "packages/cloud/lib/domain/cloudWorkspaces/types";
89
import { useGetCloudWorkspace } from "packages/cloud/services/workspaces/WorkspacesService";
@@ -11,7 +12,6 @@ import { useCurrentWorkspace } from "services/workspaces/WorkspacesService";
1112
import { ResourceNotFoundErrorBoundary } from "views/common/ResorceNotFoundErrorBoundary";
1213
import { StartOverErrorView } from "views/common/StartOverErrorView";
1314

14-
import { CreditsProblemBanner } from "./components/CreditsProblemBanner";
1515
import { InsufficientPermissionsErrorBoundary } from "./InsufficientPermissionsErrorBoundary";
1616

1717
const MainContainer = styled.div`
@@ -51,7 +51,9 @@ const MainView: React.FC = (props) => {
5151
<InsufficientPermissionsErrorBoundary errorComponent={<StartOverErrorView />}>
5252
<SideBar />
5353
<Content>
54-
{cloudWorkspace.creditStatus && showBanner && <CreditsProblemBanner status={cloudWorkspace.creditStatus} />}
54+
{cloudWorkspace.creditStatus && showBanner && (
55+
<AlertBanner alertType="credits" id={`credits.creditsProblem.${cloudWorkspace.creditStatus}`} />
56+
)}
5557
<DataBlock hasBanner={showBanner}>
5658
<ResourceNotFoundErrorBoundary errorComponent={<StartOverErrorView />}>
5759
<React.Suspense fallback={<LoadingPage />}>{props.children ?? <Outlet />}</React.Suspense>

airbyte-webapp/src/packages/cloud/views/layout/MainView/components/CreditsProblemBanner.tsx

-38
This file was deleted.

0 commit comments

Comments
 (0)