diff --git a/package-lock.json b/package-lock.json
index e470a4eb049..d4a3b909281 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5104,9 +5104,9 @@
}
},
"node_modules/@leafygreen-ui/hooks": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-7.0.0.tgz",
- "integrity": "sha512-/UDdinXJbHcNvqodfY+5Ej4auxKdzbljJx0PkLy+bAUI0/P9W4xlYb2N0LZTIZBmKj4SUSQX3O/G7fvtSqJW7A==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-7.1.0.tgz",
+ "integrity": "sha512-eL0BKaaVgVsQuyNjZqYUUJM6b42Bmdn9gSvZ473EXeNtMD3o6/VqAlYFmOWnVYWHNj8ToLXgyWNuwnRuMRZ0qg==",
"dependencies": {
"lodash": "^4.17.21"
}
@@ -5419,6 +5419,55 @@
"react-dom": "^16.0.0"
}
},
+ "node_modules/@leafygreen-ui/radio-box-group": {
+ "version": "6.1.5",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/radio-box-group/-/radio-box-group-6.1.5.tgz",
+ "integrity": "sha512-G8xOB9np41zfvx9Xv8cQ721pJAQ5vKeIkTNQbAnQGj6VgCWXHsJmPuLwo9XAxil0Sa0gAinXfgZ2Pn628kgEPA==",
+ "dependencies": {
+ "@leafygreen-ui/hooks": "^7.1.0",
+ "@leafygreen-ui/interaction-ring": "^1.1.0",
+ "@leafygreen-ui/lib": "^9.1.0",
+ "@leafygreen-ui/palette": "^3.2.2"
+ },
+ "peerDependencies": {
+ "@leafygreen-ui/leafygreen-provider": "^2.1.3"
+ }
+ },
+ "node_modules/@leafygreen-ui/radio-box-group/node_modules/@leafygreen-ui/emotion": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/emotion/-/emotion-4.0.0.tgz",
+ "integrity": "sha512-nr2g6OFsy+psaMto3H4HQ1ivM1tCwd9k1bbR5WH4U7YibDagfBekFTwlhohmC/K7hUM/eDVPGw0w4zQyC+BwZg==",
+ "dependencies": {
+ "@emotion/css": "^11.1.3",
+ "@emotion/react": "^11.4.0",
+ "@emotion/server": "^11.4.0"
+ }
+ },
+ "node_modules/@leafygreen-ui/radio-box-group/node_modules/@leafygreen-ui/lib": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/lib/-/lib-9.1.0.tgz",
+ "integrity": "sha512-JH3mnoCZNtUcJfXrvVG4I3PAIm1ehlR1H5WkDqkjengcf/iVMo7+AluzwfTCQb2fqQgohURs+qY4vEhJe+9+2g==",
+ "dependencies": {
+ "@leafygreen-ui/emotion": "^4.0.0",
+ "facepaint": "^1.2.1",
+ "polished": "^4.1.3",
+ "prop-types": "^15.0.0"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0"
+ }
+ },
+ "node_modules/@leafygreen-ui/radio-box-group/node_modules/polished": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/polished/-/polished-4.1.3.tgz",
+ "integrity": "sha512-ocPAcVBUOryJEKe0z2KLd1l9EBa1r5mSwlKpExmrLzsnIzJo4axsoU9O2BjOTkDGDT4mZ0WFE5XKTlR3nLnZOA==",
+ "dependencies": {
+ "@babel/runtime": "^7.14.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@leafygreen-ui/ripple": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@leafygreen-ui/ripple/-/ripple-1.1.1.tgz",
@@ -63156,6 +63205,7 @@
"@leafygreen-ui/modal": "^7.0.0",
"@leafygreen-ui/palette": "^3.2.2",
"@leafygreen-ui/portal": "^3.1.3",
+ "@leafygreen-ui/radio-box-group": "^6.1.4",
"@leafygreen-ui/select": "^3.0.4",
"@leafygreen-ui/tabs": "^5.1.3",
"@leafygreen-ui/text-area": "^4.0.3",
@@ -98311,17 +98361,6 @@
"decamelize": "^1.2.0"
}
},
- "packages/compass/node_modules/@mongosh/node-runtime-worker-thread": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@mongosh/node-runtime-worker-thread/-/node-runtime-worker-thread-1.1.7.tgz",
- "integrity": "sha512-CAIFF2Qi/jZcmd1YWVa9QWU4RXjwvQcMmxVsL9oUpDvD0TXrWqNcA7ws7xxQoQ5/5p790zsFmPNVA3MUdkUy3Q==",
- "dependencies": {
- "interruptor": "^1.0.1"
- },
- "engines": {
- "node": ">=12.4.0"
- }
- },
"packages/compass/node_modules/anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
@@ -98667,6 +98706,7 @@
"@mongodb-js/prettier-config-compass": "^0.3.0",
"@mongodb-js/tsconfig-compass": "^0.3.0",
"@testing-library/react": "^12.0.0",
+ "@testing-library/user-event": "^13.5.0",
"@types/chai": "^4.2.21",
"@types/chai-dom": "^0.0.10",
"@types/mocha": "^9.0.0",
@@ -111485,9 +111525,9 @@
}
},
"@leafygreen-ui/hooks": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-7.0.0.tgz",
- "integrity": "sha512-/UDdinXJbHcNvqodfY+5Ej4auxKdzbljJx0PkLy+bAUI0/P9W4xlYb2N0LZTIZBmKj4SUSQX3O/G7fvtSqJW7A==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-7.1.0.tgz",
+ "integrity": "sha512-eL0BKaaVgVsQuyNjZqYUUJM6b42Bmdn9gSvZ473EXeNtMD3o6/VqAlYFmOWnVYWHNj8ToLXgyWNuwnRuMRZ0qg==",
"requires": {
"lodash": "^4.17.21"
}
@@ -111773,6 +111813,48 @@
"@leafygreen-ui/lib": "^8.0.0"
}
},
+ "@leafygreen-ui/radio-box-group": {
+ "version": "6.1.5",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/radio-box-group/-/radio-box-group-6.1.5.tgz",
+ "integrity": "sha512-G8xOB9np41zfvx9Xv8cQ721pJAQ5vKeIkTNQbAnQGj6VgCWXHsJmPuLwo9XAxil0Sa0gAinXfgZ2Pn628kgEPA==",
+ "requires": {
+ "@leafygreen-ui/hooks": "^7.1.0",
+ "@leafygreen-ui/interaction-ring": "^1.1.0",
+ "@leafygreen-ui/lib": "^9.1.0",
+ "@leafygreen-ui/palette": "^3.2.2"
+ },
+ "dependencies": {
+ "@leafygreen-ui/emotion": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/emotion/-/emotion-4.0.0.tgz",
+ "integrity": "sha512-nr2g6OFsy+psaMto3H4HQ1ivM1tCwd9k1bbR5WH4U7YibDagfBekFTwlhohmC/K7hUM/eDVPGw0w4zQyC+BwZg==",
+ "requires": {
+ "@emotion/css": "^11.1.3",
+ "@emotion/react": "^11.4.0",
+ "@emotion/server": "^11.4.0"
+ }
+ },
+ "@leafygreen-ui/lib": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/lib/-/lib-9.1.0.tgz",
+ "integrity": "sha512-JH3mnoCZNtUcJfXrvVG4I3PAIm1ehlR1H5WkDqkjengcf/iVMo7+AluzwfTCQb2fqQgohURs+qY4vEhJe+9+2g==",
+ "requires": {
+ "@leafygreen-ui/emotion": "^4.0.0",
+ "facepaint": "^1.2.1",
+ "polished": "^4.1.3",
+ "prop-types": "^15.0.0"
+ }
+ },
+ "polished": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/polished/-/polished-4.1.3.tgz",
+ "integrity": "sha512-ocPAcVBUOryJEKe0z2KLd1l9EBa1r5mSwlKpExmrLzsnIzJo4axsoU9O2BjOTkDGDT4mZ0WFE5XKTlR3nLnZOA==",
+ "requires": {
+ "@babel/runtime": "^7.14.0"
+ }
+ }
+ }
+ },
"@leafygreen-ui/ripple": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@leafygreen-ui/ripple/-/ripple-1.1.1.tgz",
@@ -117593,6 +117675,7 @@
"@leafygreen-ui/modal": "^7.0.0",
"@leafygreen-ui/palette": "^3.2.2",
"@leafygreen-ui/portal": "^3.1.3",
+ "@leafygreen-ui/radio-box-group": "^6.1.4",
"@leafygreen-ui/select": "^3.0.4",
"@leafygreen-ui/tabs": "^5.1.3",
"@leafygreen-ui/text-area": "^4.0.3",
@@ -142339,6 +142422,7 @@
"@mongodb-js/prettier-config-compass": "^0.3.0",
"@mongodb-js/tsconfig-compass": "^0.3.0",
"@testing-library/react": "^12.0.0",
+ "@testing-library/user-event": "^13.5.0",
"@types/chai": "^4.2.21",
"@types/chai-dom": "^0.0.10",
"@types/mocha": "^9.0.0",
@@ -178184,14 +178268,6 @@
"web-vitals": "^2.1.2"
},
"dependencies": {
- "@mongosh/node-runtime-worker-thread": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@mongosh/node-runtime-worker-thread/-/node-runtime-worker-thread-1.1.7.tgz",
- "integrity": "sha512-CAIFF2Qi/jZcmd1YWVa9QWU4RXjwvQcMmxVsL9oUpDvD0TXrWqNcA7ws7xxQoQ5/5p790zsFmPNVA3MUdkUy3Q==",
- "requires": {
- "interruptor": "^1.0.1"
- }
- },
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
diff --git a/packages/compass-components/package.json b/packages/compass-components/package.json
index 48f2df49ce1..a4fdef33833 100644
--- a/packages/compass-components/package.json
+++ b/packages/compass-components/package.json
@@ -48,6 +48,7 @@
"@leafygreen-ui/modal": "^7.0.0",
"@leafygreen-ui/palette": "^3.2.2",
"@leafygreen-ui/portal": "^3.1.3",
+ "@leafygreen-ui/radio-box-group": "^6.1.4",
"@leafygreen-ui/select": "^3.0.4",
"@leafygreen-ui/tabs": "^5.1.3",
"@leafygreen-ui/text-area": "^4.0.3",
diff --git a/packages/compass-components/src/compass-ui-colors.tsx b/packages/compass-components/src/compass-ui-colors.tsx
index 882d672678f..6c60907265f 100644
--- a/packages/compass-components/src/compass-ui-colors.tsx
+++ b/packages/compass-components/src/compass-ui-colors.tsx
@@ -1,2 +1,3 @@
export const gray8 = '#f5f6f7';
export const transparentGray = 'rgba(180, 180, 180, 0.5)';
+export const transparentWhite = 'rgba(255, 255, 255, 0.5)';
diff --git a/packages/compass-components/src/components/accordion.tsx b/packages/compass-components/src/components/accordion.tsx
index 4d5a68ed09b..1a91408f396 100644
--- a/packages/compass-components/src/components/accordion.tsx
+++ b/packages/compass-components/src/components/accordion.tsx
@@ -27,9 +27,9 @@ const containerStyles = css({
marginTop: spacing[3],
display: 'flex',
alignItems: 'center',
- '&:hover': {
- cursor: 'pointer',
- },
+});
+const buttonIconStyles = css({
+ marginRight: spacing[1],
});
interface AccordionProps {
dataTestId?: string;
@@ -54,7 +54,10 @@ function Accordion(
setOpen((currentOpen) => !currentOpen);
}}
>
-
+
{props.text}
diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts
index 76f5dbdbda1..a18945a3aca 100644
--- a/packages/compass-components/src/index.ts
+++ b/packages/compass-components/src/index.ts
@@ -32,6 +32,7 @@ export { default as Modal } from '@leafygreen-ui/modal';
export { uiColors } from '@leafygreen-ui/palette';
export * as compassUIColors from './compass-ui-colors';
export { default as Portal } from '@leafygreen-ui/portal';
+export { RadioBox, RadioBoxGroup } from '@leafygreen-ui/radio-box-group';
export { Select, Option, Size as SelectSize } from '@leafygreen-ui/select';
export { Tabs, Tab } from '@leafygreen-ui/tabs';
export { default as TextArea } from '@leafygreen-ui/text-area';
diff --git a/packages/connect-form/package.json b/packages/connect-form/package.json
index 1c2f1d1bc66..e4f5d97caa9 100644
--- a/packages/connect-form/package.json
+++ b/packages/connect-form/package.json
@@ -65,6 +65,7 @@
"@mongodb-js/prettier-config-compass": "^0.3.0",
"@mongodb-js/tsconfig-compass": "^0.3.0",
"@testing-library/react": "^12.0.0",
+ "@testing-library/user-event": "^13.5.0",
"@types/chai": "^4.2.21",
"@types/chai-dom": "^0.0.10",
"@types/mocha": "^9.0.0",
diff --git a/packages/connect-form/src/advanced-connection-options.tsx b/packages/connect-form/src/advanced-connection-options.tsx
deleted file mode 100644
index 7f674aa4697..00000000000
--- a/packages/connect-form/src/advanced-connection-options.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-import { Accordion } from '@mongodb-js/compass-components';
-import AdvancedOptionsTabs from './advanced-options-tabs/advanced-options-tabs';
-
-function AdvancedConnectionOptions(): React.ReactElement {
- return (
-
-
-
- );
-}
-
-export default AdvancedConnectionOptions;
diff --git a/packages/connect-form/src/advanced-options-tabs/advanced-options-tabs.tsx b/packages/connect-form/src/advanced-options-tabs/advanced-options-tabs.tsx
deleted file mode 100644
index bab53a4df62..00000000000
--- a/packages/connect-form/src/advanced-options-tabs/advanced-options-tabs.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React, { useState } from 'react';
-import { Tabs, Tab } from '@mongodb-js/compass-components';
-
-import GeneralTab from './general-tab';
-import SSLTab from './ssl-tab';
-import SSHTunnelTab from './ssh-tunnel-tab';
-import AdvancedTab from './advanced-tab';
-
-interface TabObject {
- name: string;
- component: React.FunctionComponent;
-}
-
-function renderTab(tabObject: TabObject, idx: number): React.ReactElement {
- const TabComponent = tabObject.component;
- return (
-
-
-
- );
-}
-function AdvancedOptionsTabs(): React.ReactElement {
- const [activeTab, setActiveTab] = useState(0);
-
- const tabs: TabObject[] = [
- { name: 'General', component: GeneralTab },
- { name: 'SSL', component: SSLTab },
- { name: 'SSH Tunnel', component: SSHTunnelTab },
- { name: 'Advanced', component: AdvancedTab },
- ];
- return (
-
- {tabs.map(renderTab)}
-
- );
-}
-
-export default AdvancedOptionsTabs;
diff --git a/packages/connect-form/src/advanced-options-tabs/general-tab.tsx b/packages/connect-form/src/advanced-options-tabs/general-tab.tsx
deleted file mode 100644
index c9d652b67d3..00000000000
--- a/packages/connect-form/src/advanced-options-tabs/general-tab.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-function GeneralTab(): React.ReactElement {
- return General
;
-}
-
-export default GeneralTab;
diff --git a/packages/connect-form/src/components/advanced-connection-options.tsx b/packages/connect-form/src/components/advanced-connection-options.tsx
new file mode 100644
index 00000000000..adddb00d4dc
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-connection-options.tsx
@@ -0,0 +1,63 @@
+import { css } from '@emotion/css';
+import React from 'react';
+import {
+ Accordion,
+ compassUIColors,
+ spacing,
+} from '@mongodb-js/compass-components';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import AdvancedOptionsTabs from './advanced-options-tabs/advanced-options-tabs';
+import { UpdateConnectionFormField } from '../hooks/use-connect-form';
+import { ConnectionFormError } from '../utils/connect-form-errors';
+
+const disabledOverlayStyles = css({
+ position: 'absolute',
+ top: 0,
+ // Space around it to ensure added focus borders are covered.
+ bottom: -spacing[1],
+ left: -spacing[1],
+ right: -spacing[1],
+ backgroundColor: compassUIColors.transparentWhite,
+ zIndex: 1,
+ cursor: 'not-allowed',
+});
+
+const connectionTabsContainer = css({
+ position: 'relative',
+});
+
+function AdvancedConnectionOptions({
+ disabled,
+ errors,
+ connectionStringUrl,
+ hideError,
+ updateConnectionFormField,
+}: {
+ errors: ConnectionFormError[];
+ disabled: boolean;
+ connectionStringUrl: ConnectionStringUrl;
+ hideError: (errorIndex: number) => void;
+ updateConnectionFormField: UpdateConnectionFormField;
+}): React.ReactElement {
+ return (
+
+
+ {disabled && (
+
+ )}
+
+
+
+ );
+}
+
+export default AdvancedConnectionOptions;
diff --git a/packages/connect-form/src/components/advanced-options-tabs/advanced-options-tabs.tsx b/packages/connect-form/src/components/advanced-options-tabs/advanced-options-tabs.tsx
new file mode 100644
index 00000000000..a88e59ef7a7
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/advanced-options-tabs.tsx
@@ -0,0 +1,73 @@
+import { css } from '@emotion/css';
+import React, { useState } from 'react';
+import { Tabs, Tab, spacing } from '@mongodb-js/compass-components';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import GeneralTab from './general-tab';
+import AuthenticationTab from './authentication-tab';
+import SSLTab from './ssl-tab';
+import SSHTunnelTab from './ssh-tunnel-tab';
+import AdvancedTab from './advanced-tab';
+import { UpdateConnectionFormField } from '../../hooks/use-connect-form';
+import { ConnectionFormError } from '../../utils/connect-form-errors';
+
+const tabsStyles = css({
+ marginTop: spacing[1],
+});
+interface TabObject {
+ name: string;
+ component: React.FunctionComponent<{
+ errors: ConnectionFormError[];
+ connectionStringUrl: ConnectionStringUrl;
+ hideError: (errorIndex: number) => void;
+ updateConnectionFormField: UpdateConnectionFormField;
+ }>;
+}
+
+function AdvancedOptionsTabs({
+ errors,
+ connectionStringUrl,
+ hideError,
+ updateConnectionFormField,
+}: {
+ errors: ConnectionFormError[];
+ connectionStringUrl: ConnectionStringUrl;
+ hideError: (errorIndex: number) => void;
+ updateConnectionFormField: UpdateConnectionFormField;
+}): React.ReactElement {
+ const [activeTab, setActiveTab] = useState(0);
+
+ const tabs: TabObject[] = [
+ { name: 'General', component: GeneralTab },
+ { name: 'Authentication', component: AuthenticationTab },
+ { name: 'TLS/SSL', component: SSLTab },
+ { name: 'SSH Tunnel', component: SSHTunnelTab },
+ { name: 'Advanced', component: AdvancedTab },
+ ];
+
+ return (
+
+ {tabs.map((tabObject: TabObject, idx: number) => {
+ const TabComponent = tabObject.component;
+
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
+
+export default AdvancedOptionsTabs;
diff --git a/packages/connect-form/src/advanced-options-tabs/advanced-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/advanced-tab.tsx
similarity index 100%
rename from packages/connect-form/src/advanced-options-tabs/advanced-tab.tsx
rename to packages/connect-form/src/components/advanced-options-tabs/advanced-tab.tsx
diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab.tsx
new file mode 100644
index 00000000000..da7a17e1215
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+function AuthenticationTab(): React.ReactElement {
+ return Authentication
;
+}
+
+export default AuthenticationTab;
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general-tab.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/general-tab.spec.tsx
new file mode 100644
index 00000000000..da236ebd0a4
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/general-tab.spec.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { expect } from 'chai';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import GeneralTab from './general-tab';
+
+const noop = () => {
+ /* */
+};
+
+describe('GeneralTab', function () {
+ describe('with a srv connection string schema (mongodb+srv://)', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb+srv://0ranges:p!neapp1es@localhost/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('should not render the direct connection input', function () {
+ expect(screen.queryByText('Direct Connection')).to.not.exist;
+ });
+
+ it('should render the schema input', function () {
+ expect(screen.getByText('Schema')).to.be.visible;
+ });
+
+ it('should render the hostname input', function () {
+ expect(screen.getByText('Hostname')).to.be.visible;
+ });
+ });
+
+ describe('with a standard connection string schema (mongodb://) with one host', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://0ranges:p!neapp1es@localhost:27107/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('should render the direct connection input', function () {
+ expect(screen.getByText('Direct Connection')).to.be.visible;
+ });
+ });
+
+ describe('with a standard connection string schema (mongodb://) with multiple hosts', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://0ranges:p!neapp1es@localhost:27017,localhost:27019/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('should not render the direct connection input', function () {
+ expect(screen.queryByText('Direct Connection')).to.not.exist;
+ });
+ });
+
+ describe('standard schema (mongodb://) with multiple hosts and directConnection=true', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://0ranges:p!neapp1es@localhost:27017,localhost:27019/?ssl=true&directConnection=true'
+ );
+ render(
+
+ );
+ });
+
+ it('should not render the direct connection input', function () {
+ expect(screen.queryByText('Direct Connection')).to.be.visible;
+ });
+ });
+});
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/general-tab.tsx
new file mode 100644
index 00000000000..82330d1b209
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/general-tab.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import SchemaInput from './general/schema-input';
+import { UpdateConnectionFormField } from '../../hooks/use-connect-form';
+import { ConnectionFormError } from '../../utils/connect-form-errors';
+import FormFieldContainer from '../form-field-container';
+import HostInput from './general/host-input';
+
+function GeneralTab({
+ errors,
+ connectionStringUrl,
+ hideError,
+ updateConnectionFormField,
+}: {
+ errors: ConnectionFormError[];
+ connectionStringUrl: ConnectionStringUrl;
+ hideError: (errorIndex: number) => void;
+ updateConnectionFormField: UpdateConnectionFormField;
+}): React.ReactElement {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default GeneralTab;
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general/direct-connection-input.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/general/direct-connection-input.spec.tsx
new file mode 100644
index 00000000000..24f18f4ad73
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/general/direct-connection-input.spec.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { render, screen, cleanup, fireEvent } from '@testing-library/react';
+import { expect } from 'chai';
+import sinon from 'sinon';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import DirectConnectionInput from './direct-connection-input';
+
+describe('DirectConnectionInput', function () {
+ let updateConnectionFormFieldSpy: sinon.SinonSpy;
+
+ beforeEach(function () {
+ updateConnectionFormFieldSpy = sinon.spy();
+ });
+
+ afterEach(cleanup);
+
+ describe('when directConnection is "true" on the connection string', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://localhost:27019/?directConnection=true&ssl=false'
+ );
+ render(
+
+ );
+ });
+
+ it('should render checked', function () {
+ const checkbox: HTMLInputElement = screen.getByRole('checkbox');
+ expect(checkbox.checked).to.equal(true);
+ });
+
+ describe('when the checkbox is clicked', function () {
+ beforeEach(function () {
+ const checkbox: HTMLInputElement = screen.getByRole('checkbox');
+ fireEvent.click(checkbox);
+ });
+
+ it('should call to update with direct connection = false', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-direct-connection',
+ isDirectConnection: false,
+ });
+ });
+ });
+ });
+
+ describe('when directConnection is unset on the connection string', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://localhost:27019/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('should render not checked', function () {
+ const checkbox: HTMLInputElement = screen.getByRole('checkbox');
+ expect(checkbox.checked).to.equal(false);
+ });
+
+ describe('when the checkbox is clicked', function () {
+ beforeEach(function () {
+ const checkbox: HTMLInputElement = screen.getByRole('checkbox');
+ fireEvent.click(checkbox);
+ });
+
+ it('should call to update with direct connection = true', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-direct-connection',
+ isDirectConnection: true,
+ });
+ });
+ });
+ });
+
+ describe('when directConnection is "NOT_TRUE_OR_FALSE" on the connection string', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://localhost:27019/?directConnection=NOT_TRUE_OR_FALSE&ssl=false'
+ );
+ render(
+
+ );
+ });
+
+ it('should render not checked', function () {
+ const checkbox: HTMLInputElement = screen.getByRole('checkbox');
+ expect(checkbox.checked).to.equal(false);
+ });
+
+ describe('when the checkbox is clicked', function () {
+ beforeEach(function () {
+ const checkbox: HTMLInputElement = screen.getByRole('checkbox');
+ fireEvent.click(checkbox);
+ });
+
+ it('should call to update with direct connection = true', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-direct-connection',
+ isDirectConnection: true,
+ });
+ });
+ });
+ });
+});
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general/direct-connection-input.tsx b/packages/connect-form/src/components/advanced-options-tabs/general/direct-connection-input.tsx
new file mode 100644
index 00000000000..05b665114aa
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/general/direct-connection-input.tsx
@@ -0,0 +1,43 @@
+import React, { useCallback } from 'react';
+import { Checkbox, Description } from '@mongodb-js/compass-components';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import { UpdateConnectionFormField } from '../../../hooks/use-connect-form';
+
+function DirectConnectionInput({
+ connectionStringUrl,
+ updateConnectionFormField,
+}: {
+ connectionStringUrl: ConnectionStringUrl;
+ updateConnectionFormField: UpdateConnectionFormField;
+}): React.ReactElement {
+ const isDirectConnection =
+ connectionStringUrl.searchParams.get('directConnection') === 'true';
+
+ const updateDirectConnection = useCallback(
+ (event: React.ChangeEvent) => {
+ updateConnectionFormField({
+ type: 'update-direct-connection',
+ isDirectConnection: event.target.checked,
+ });
+ },
+ [updateConnectionFormField]
+ );
+
+ return (
+ <>
+
+
+ Specifies whether to force dispatch all operations to the specified
+ host.
+
+ >
+ );
+}
+
+export default DirectConnectionInput;
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general/host-input.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/general/host-input.spec.tsx
new file mode 100644
index 00000000000..fc3427cbbca
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/general/host-input.spec.tsx
@@ -0,0 +1,208 @@
+import React from 'react';
+import { render, screen, cleanup, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { expect } from 'chai';
+import sinon from 'sinon';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import HostInput from './host-input';
+import { MARKABLE_FORM_FIELD_NAMES } from '../../../constants/markable-form-fields';
+
+describe('HostInput', function () {
+ let updateConnectionFormFieldSpy: sinon.SinonSpy;
+
+ beforeEach(function () {
+ updateConnectionFormFieldSpy = sinon.spy();
+ });
+
+ afterEach(cleanup);
+
+ describe('connection string srv schema (mongodb+srv://)', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb+srv://0ranges:p!neapp1es@outerspace/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('renders a host input', function () {
+ expect(screen.getByRole('textbox')).to.exist;
+ });
+
+ it('renders the host value in the input', function () {
+ expect(screen.getByRole('textbox').getAttribute('value')).to.equal(
+ 'outerspace'
+ );
+ });
+
+ it('does not render a plus button to add more hosts', function () {
+ expect(screen.queryByRole('button')).to.not.exist;
+ });
+
+ describe('when the host is updated', function () {
+ beforeEach(function () {
+ userEvent.tab();
+ userEvent.keyboard('s');
+ });
+
+ it('should call to update the hosts', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-host',
+ hostIndex: 0,
+ newHostValue: 'outerspaces',
+ });
+ });
+ });
+
+ describe('when the host is updated to an invalid value', function () {
+ beforeEach(function () {
+ userEvent.tab();
+ userEvent.keyboard('@');
+ });
+
+ it('should call to update the connection string url', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-host',
+ hostIndex: 0,
+ newHostValue: 'outerspace@',
+ });
+ });
+ });
+ });
+
+ describe('when the host has an error', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://0ranges:p!neapp1es@outerspace:27017,outerspace:27099,outerspace:27098,localhost:27098/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('renders the error', function () {
+ expect(screen.getByText('Eeeee!!!')).to.be.visible;
+ });
+ });
+
+ describe('connection string standard schema (mongodb://)', function () {
+ describe('with a single host', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://0ranges:p!neapp1es@outerspace:27019/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('does not render the remove host', function () {
+ expect(screen.queryByLabelText('Remove host')).to.not.exist;
+ });
+
+ describe('when the host is changed', function () {
+ beforeEach(function () {
+ const hostInput = screen.getByRole('textbox');
+ userEvent.click(hostInput);
+ userEvent.keyboard('7');
+ });
+
+ it('should call to update the connection string url with the updated host', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-host',
+ hostIndex: 0,
+ newHostValue: 'outerspace:270197',
+ });
+ });
+ });
+ });
+
+ describe('with multiple hosts', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://0ranges:p!neapp1es@outerspace:27017,outerspace:27098,outerspace:27099,localhost:27098/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('renders inputs for all of the hosts', function () {
+ expect(screen.getAllByRole('textbox').length).to.equal(4);
+ });
+
+ describe('when the remove host button is clicked', function () {
+ beforeEach(function () {
+ const removeHostButton = screen.getAllByLabelText('Remove host')[2];
+ fireEvent.click(removeHostButton);
+ });
+
+ it('should call to remove the host', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'remove-host',
+ hostIndexToRemove: 2,
+ });
+ });
+ });
+
+ describe('when the add host button is clicked', function () {
+ beforeEach(function () {
+ const addHostButton = screen.getAllByLabelText('Add new host')[2];
+ fireEvent.click(addHostButton);
+ });
+
+ it('should call to a new host at the location', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'add-new-host',
+ hostIndexToAddAfter: 2,
+ });
+ });
+ });
+
+ describe('when a host is changed', function () {
+ beforeEach(function () {
+ const hostInput = screen.getAllByRole('textbox')[2];
+ userEvent.click(hostInput);
+ userEvent.keyboard('8');
+ });
+
+ it('should call to update the connection string url with the updated host', function () {
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-host',
+ hostIndex: 2,
+ newHostValue: 'outerspace:270998',
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general/host-input.tsx b/packages/connect-form/src/components/advanced-options-tabs/general/host-input.tsx
new file mode 100644
index 00000000000..7a7e623a514
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/general/host-input.tsx
@@ -0,0 +1,145 @@
+import { css } from '@emotion/css';
+import React, { useCallback, useEffect, useState } from 'react';
+import {
+ Label,
+ Icon,
+ IconButton,
+ TextInput,
+ spacing,
+} from '@mongodb-js/compass-components';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import { UpdateConnectionFormField } from '../../../hooks/use-connect-form';
+import { ConnectionFormError } from '../../../utils/connect-form-errors';
+import { MARKABLE_FORM_FIELD_NAMES } from '../../../constants/markable-form-fields';
+import DirectConnectionInput from './direct-connection-input';
+import FormFieldContainer from '../../form-field-container';
+
+const hostInputContainerStyles = css({
+ display: 'flex',
+ flexDirection: 'row',
+ width: '100%',
+ marginBottom: spacing[2],
+});
+
+const hostInputStyles = css({
+ flexGrow: 1,
+});
+
+const hostActionButtonStyles = css({
+ marginLeft: spacing[1],
+ marginTop: spacing[1],
+});
+
+function HostInput({
+ errors,
+ connectionStringUrl,
+ updateConnectionFormField,
+}: {
+ errors: ConnectionFormError[];
+ connectionStringUrl: ConnectionStringUrl;
+ updateConnectionFormField: UpdateConnectionFormField;
+}): React.ReactElement {
+ const [hosts, setHosts] = useState([...connectionStringUrl.hosts]);
+ const { isSRV } = connectionStringUrl;
+
+ const showDirectConnectionInput =
+ connectionStringUrl.searchParams.get('directConnection') === 'true' ||
+ (!connectionStringUrl.isSRV && hosts.length === 1);
+
+ useEffect(() => {
+ // Update the hosts in the state when the underlying connection string hosts
+ // change. This can be when a user changes connections, pastes in a new
+ // connection string, or changes a setting which also updates the hosts.
+ setHosts([...connectionStringUrl.hosts]);
+ }, [connectionStringUrl]);
+
+ const onHostChange = useCallback(
+ (event: React.ChangeEvent, index: number) => {
+ const newHosts = [...hosts];
+ newHosts[index] = event.target.value || '';
+
+ setHosts(newHosts);
+ updateConnectionFormField({
+ type: 'update-host',
+ hostIndex: index,
+ newHostValue: event.target.value,
+ });
+ },
+ [hosts, setHosts, updateConnectionFormField]
+ );
+
+ const hostsErrorIndex = errors.findIndex(
+ (error) => error.fieldName === MARKABLE_FORM_FIELD_NAMES.HOSTS
+ );
+ const hostsError = errors[hostsErrorIndex];
+
+ return (
+ <>
+
+
+ {hosts.map((host, index) => (
+
+ onHostChange(e, index)}
+ />
+ {!isSRV && (
+
+ updateConnectionFormField({
+ type: 'add-new-host',
+ hostIndexToAddAfter: index,
+ })
+ }
+ >
+
+
+ )}
+ {!isSRV && hosts.length > 1 && (
+
+ updateConnectionFormField({
+ type: 'remove-host',
+ hostIndexToRemove: index,
+ })
+ }
+ >
+
+
+ )}
+
+ ))}
+
+ {showDirectConnectionInput && (
+
+
+
+ )}
+ >
+ );
+}
+
+export default HostInput;
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general/schema-input.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/general/schema-input.spec.tsx
new file mode 100644
index 00000000000..efc07dac4a4
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/general/schema-input.spec.tsx
@@ -0,0 +1,165 @@
+import React from 'react';
+import { render, screen, cleanup, fireEvent } from '@testing-library/react';
+import { expect } from 'chai';
+import sinon from 'sinon';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import SchemaInput from './schema-input';
+import { MARKABLE_FORM_FIELD_NAMES } from '../../../constants/markable-form-fields';
+
+describe('SchemaInput', function () {
+ let updateConnectionFormFieldSpy: sinon.SinonSpy;
+
+ beforeEach(function () {
+ updateConnectionFormFieldSpy = sinon.spy();
+ });
+
+ afterEach(cleanup);
+
+ describe('with a srv connection string schema (mongodb+srv://)', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb+srv://0ranges:p!neapp1es@localhost/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('should render the srv box selected', function () {
+ const srvRadioBox = screen.getAllByRole('radio')[1] as HTMLInputElement;
+ expect(srvRadioBox.checked).to.equal(true);
+ expect(srvRadioBox.getAttribute('aria-checked')).to.equal('true');
+ });
+
+ it('should render the standard box not selected', function () {
+ const standardSchemaRadioBox = screen.getAllByRole(
+ 'radio'
+ )[0] as HTMLInputElement;
+ expect(standardSchemaRadioBox.checked).to.equal(false);
+ expect(standardSchemaRadioBox.getAttribute('aria-checked')).to.equal(
+ 'false'
+ );
+ });
+
+ describe('when the standard schema radio box is clicked', function () {
+ beforeEach(function () {
+ const standardSchemaRadioBox = screen.getAllByRole('radio')[0];
+ fireEvent.click(standardSchemaRadioBox);
+ });
+
+ it('should call to update the connection string with standard schema', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-connection-schema',
+ isSrv: false,
+ });
+ });
+ });
+
+ describe('when the srv radio box is clicked again', function () {
+ beforeEach(function () {
+ const srvRadioBox = screen.getAllByRole('radio')[1];
+ fireEvent.click(srvRadioBox);
+ });
+
+ it('should not call to update the field string', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(0);
+ });
+ });
+ });
+
+ describe('with a standard connection string schema (mongodb://)', function () {
+ describe('with a single host', function () {
+ beforeEach(function () {
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://0ranges:p!neapp1es@outerspace:27017/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('should render the standard selected', function () {
+ const srvRadioBox = screen.getAllByRole('radio')[0] as HTMLInputElement;
+ expect(srvRadioBox.checked).to.equal(true);
+ expect(srvRadioBox.getAttribute('aria-checked')).to.equal('true');
+ });
+
+ it('should render the srv box not selected', function () {
+ const srvRadioBox = screen.getAllByRole('radio')[1] as HTMLInputElement;
+ expect(srvRadioBox.checked).to.equal(false);
+ expect(srvRadioBox.getAttribute('aria-checked')).to.equal('false');
+ });
+
+ describe('when the srv schema radio box is clicked', function () {
+ beforeEach(function () {
+ const srvSchemaRadioBox = screen.getAllByRole('radio')[1];
+ fireEvent.click(srvSchemaRadioBox);
+ });
+
+ it('should call to update the connection string with srv schema', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-connection-schema',
+ isSrv: true,
+ });
+ });
+ });
+ });
+ });
+
+ describe('when there is a schema error', function () {
+ let hideErrorSpy: sinon.SinonSpy;
+
+ beforeEach(function () {
+ hideErrorSpy = sinon.spy();
+ const connectionStringUrl = new ConnectionStringUrl(
+ 'mongodb://0ranges:p!neapp1es@outerspace:27017/?ssl=true'
+ );
+ render(
+
+ );
+ });
+
+ it('should render the schema conversion error', function () {
+ expect(screen.getByText('aaaa!!!1!')).to.be.visible;
+ });
+
+ describe('when the x button is clicked', function () {
+ beforeEach(function () {
+ const hideErrorButton = screen.getByLabelText('X Icon');
+ fireEvent.click(hideErrorButton);
+ });
+
+ it('should call to hide the error with the correct index', function () {
+ expect(hideErrorSpy.callCount).to.equal(1);
+ expect(hideErrorSpy.firstCall.args[0]).to.equal(1);
+ });
+ });
+ });
+});
diff --git a/packages/connect-form/src/components/advanced-options-tabs/general/schema-input.tsx b/packages/connect-form/src/components/advanced-options-tabs/general/schema-input.tsx
new file mode 100644
index 00000000000..ca6c9731edb
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/general/schema-input.tsx
@@ -0,0 +1,87 @@
+import { css } from '@emotion/css';
+import React, { useCallback } from 'react';
+import {
+ Banner,
+ BannerVariant,
+ Description,
+ Label,
+ RadioBox,
+ RadioBoxGroup,
+ spacing,
+} from '@mongodb-js/compass-components';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+
+import { UpdateConnectionFormField } from '../../../hooks/use-connect-form';
+import { ConnectionFormError } from '../../../utils/connect-form-errors';
+import { MARKABLE_FORM_FIELD_NAMES } from '../../../constants/markable-form-fields';
+
+enum MONGODB_SCHEMA {
+ MONGODB = 'MONGODB',
+ MONGODB_SRV = 'MONGODB_SRV',
+}
+
+const descriptionStyles = css({
+ marginTop: spacing[1],
+});
+
+const regularSchemaDescription =
+ 'Standard Connection String Format. The standard format of the MongoDB connection URI is used to connect to a MongoDB deployment: standalone, replica set, or a sharded cluster.';
+const srvSchemaDescription =
+ 'DNS Seed List Connection Format. The +srv indicates to the client that the hostname that follows corresponds to a DNS SRV record.';
+
+function SchemaInput({
+ connectionStringUrl,
+ errors,
+ hideError,
+ updateConnectionFormField,
+}: {
+ connectionStringUrl: ConnectionStringUrl;
+ errors: ConnectionFormError[];
+ hideError: (errorIndex: number) => void;
+ updateConnectionFormField: UpdateConnectionFormField;
+}): React.ReactElement {
+ const { isSRV } = connectionStringUrl;
+
+ const onChangeConnectionSchema = useCallback(
+ (event: React.ChangeEvent) => {
+ updateConnectionFormField({
+ type: 'update-connection-schema',
+ isSrv: event.target.value === MONGODB_SCHEMA.MONGODB_SRV,
+ });
+ },
+ [updateConnectionFormField]
+ );
+
+ const schemaUpdateErrorIndex = errors.findIndex(
+ (error) => error.fieldName === MARKABLE_FORM_FIELD_NAMES.IS_SRV
+ );
+ const schemaUpdateError = errors[schemaUpdateErrorIndex];
+
+ return (
+ <>
+
+
+ mongodb
+ mongodb+srv
+
+
+ {isSRV ? srvSchemaDescription : regularSchemaDescription}
+
+ {schemaUpdateError && (
+ hideError(schemaUpdateErrorIndex)}
+ >
+ {schemaUpdateError.message}
+
+ )}
+ >
+ );
+}
+
+export default SchemaInput;
diff --git a/packages/connect-form/src/advanced-options-tabs/ssh-tunnel-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab.tsx
similarity index 100%
rename from packages/connect-form/src/advanced-options-tabs/ssh-tunnel-tab.tsx
rename to packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab.tsx
diff --git a/packages/connect-form/src/advanced-options-tabs/ssl-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/ssl-tab.tsx
similarity index 77%
rename from packages/connect-form/src/advanced-options-tabs/ssl-tab.tsx
rename to packages/connect-form/src/components/advanced-options-tabs/ssl-tab.tsx
index 4b188187d5d..47e4b0e131c 100644
--- a/packages/connect-form/src/advanced-options-tabs/ssl-tab.tsx
+++ b/packages/connect-form/src/components/advanced-options-tabs/ssl-tab.tsx
@@ -1,7 +1,7 @@
import React from 'react';
function SSLTab(): React.ReactElement {
- return SSL
;
+ return SSL/TLS
;
}
export default SSLTab;
diff --git a/packages/connect-form/src/confirm-edit-connection-string.spec.tsx b/packages/connect-form/src/components/confirm-edit-connection-string.spec.tsx
similarity index 100%
rename from packages/connect-form/src/confirm-edit-connection-string.spec.tsx
rename to packages/connect-form/src/components/confirm-edit-connection-string.spec.tsx
diff --git a/packages/connect-form/src/confirm-edit-connection-string.tsx b/packages/connect-form/src/components/confirm-edit-connection-string.tsx
similarity index 100%
rename from packages/connect-form/src/confirm-edit-connection-string.tsx
rename to packages/connect-form/src/components/confirm-edit-connection-string.tsx
diff --git a/packages/connect-form/src/components/connect-form-actions.tsx b/packages/connect-form/src/components/connect-form-actions.tsx
new file mode 100644
index 00000000000..fe7e86504c4
--- /dev/null
+++ b/packages/connect-form/src/components/connect-form-actions.tsx
@@ -0,0 +1,30 @@
+import { css } from '@emotion/css';
+import React from 'react';
+import {
+ Button,
+ ButtonVariant,
+ spacing,
+ uiColors,
+} from '@mongodb-js/compass-components';
+
+const formActionStyles = css({
+ padding: spacing[4],
+ borderTop: `1px solid ${uiColors.gray.light2}`,
+ textAlign: 'right',
+});
+
+function ConnectFormActions({
+ onConnectClicked,
+}: {
+ onConnectClicked: () => void;
+}): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export default ConnectFormActions;
diff --git a/packages/connect-form/src/connect-form.spec.tsx b/packages/connect-form/src/components/connect-form.spec.tsx
similarity index 98%
rename from packages/connect-form/src/connect-form.spec.tsx
rename to packages/connect-form/src/components/connect-form.spec.tsx
index a68fbe3e6fc..4eaf7277b16 100644
--- a/packages/connect-form/src/connect-form.spec.tsx
+++ b/packages/connect-form/src/components/connect-form.spec.tsx
@@ -11,6 +11,7 @@ function renderForm() {
/* */
}}
initialConnectionInfo={{
+ id: 'test',
connectionOptions: {
connectionString: 'mongodb://pineapple:orangutans@localhost:27019',
},
diff --git a/packages/connect-form/src/components/connect-form.tsx b/packages/connect-form/src/components/connect-form.tsx
new file mode 100644
index 00000000000..7a1d320a62b
--- /dev/null
+++ b/packages/connect-form/src/components/connect-form.tsx
@@ -0,0 +1,106 @@
+import { css } from '@emotion/css';
+import React from 'react';
+import { ConnectionInfo } from 'mongodb-data-service';
+import {
+ Banner,
+ BannerVariant,
+ Card,
+ Description,
+ H3,
+ spacing,
+} from '@mongodb-js/compass-components';
+
+import ConnectionStringInput from './connection-string-input';
+import AdvancedConnectionOptions from './advanced-connection-options';
+import ConnectFormActions from './connect-form-actions';
+import { useConnectForm } from '../hooks/use-connect-form';
+
+const formContainerStyles = css({
+ margin: 0,
+ padding: 0,
+ height: 'fit-content',
+ flexGrow: 1,
+ minWidth: 650,
+ maxWidth: 760,
+ position: 'relative',
+ display: 'inline-block',
+});
+
+const formCardStyles = css({
+ margin: 0,
+ padding: spacing[2],
+ height: 'fit-content',
+ width: '100%',
+ position: 'relative',
+});
+
+const descriptionStyles = css({
+ marginTop: spacing[2],
+});
+
+const formContentContainerStyles = css({
+ padding: spacing[4],
+});
+
+function ConnectForm({
+ initialConnectionInfo,
+ onConnectClicked,
+}: {
+ initialConnectionInfo: ConnectionInfo;
+ onConnectClicked: (connectionInfo: ConnectionInfo) => void;
+}): React.ReactElement {
+ const [
+ { errors, connectionStringUrl, connectionStringInvalidError },
+ {
+ updateConnectionFormField,
+ setConnectionStringUrl,
+ setConnectionStringError,
+ hideError,
+ },
+ ] = useConnectForm(initialConnectionInfo);
+
+ const editingConnectionStringUrl = connectionStringUrl;
+
+ return (
+
+
+
+
New Connection
+
+ Connect to a MongoDB deployment
+
+
+ {connectionStringInvalidError && (
+
+ {connectionStringInvalidError}
+
+ )}
+
+
+
+ onConnectClicked({
+ ...initialConnectionInfo,
+ connectionOptions: {
+ ...initialConnectionInfo.connectionOptions,
+ connectionString: editingConnectionStringUrl.toString(),
+ },
+ })
+ }
+ />
+
+
+ );
+}
+
+export default ConnectForm;
diff --git a/packages/connect-form/src/connection-string-input.spec.tsx b/packages/connect-form/src/components/connection-string-input.spec.tsx
similarity index 69%
rename from packages/connect-form/src/connection-string-input.spec.tsx
rename to packages/connect-form/src/components/connection-string-input.spec.tsx
index 22e6fe3380f..6bf54a3c852 100644
--- a/packages/connect-form/src/connection-string-input.spec.tsx
+++ b/packages/connect-form/src/components/connection-string-input.spec.tsx
@@ -1,5 +1,12 @@
import React from 'react';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+ cleanup,
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import sinon from 'sinon';
@@ -8,7 +15,14 @@ import ConnectionStringInput, {
} from './connection-string-input';
describe('ConnectionStringInput Component', function () {
- let setConnectionStringSpy: sinon.SinonSpy;
+ let setConnectionStringErrorSpy: sinon.SinonSpy;
+ let setConnectionStringUrlSpy: sinon.SinonSpy;
+
+ beforeEach(function () {
+ setConnectionStringErrorSpy = sinon.spy();
+ setConnectionStringUrlSpy = sinon.spy();
+ });
+ afterEach(cleanup);
describe('#hidePasswordInConnectionString', function () {
it('returns the connection string when it cannot be parsed', function () {
@@ -49,20 +63,15 @@ describe('ConnectionStringInput Component', function () {
describe('with an empty connection string', function () {
beforeEach(function () {
- setConnectionStringSpy = sinon.spy();
-
render(
);
});
- afterEach(function () {
- setConnectionStringSpy = null;
- });
-
it('should show the connection string in the text area', function () {
const textArea = screen.getByRole('textbox');
expect(textArea).to.have.text('');
@@ -72,16 +81,53 @@ describe('ConnectionStringInput Component', function () {
const textArea = screen.getByRole('textbox');
expect(textArea).to.not.match('[disabled]');
});
+
+ describe('when an invalid connection string is inputted', function () {
+ beforeEach(function () {
+ // Focus the input.
+ userEvent.tab();
+ userEvent.tab();
+ userEvent.tab();
+ userEvent.keyboard('z');
+ });
+
+ it('should call setConnectionStringError', function () {
+ expect(setConnectionStringErrorSpy.callCount).to.equal(1);
+ expect(setConnectionStringErrorSpy.firstCall.args[0]).to.equal(
+ 'Invalid schema, expected connection string to start with `mongodb://` or `mongodb+srv://`'
+ );
+ });
+
+ it('should not call setConnectionStringUrl', function () {
+ expect(setConnectionStringUrlSpy.callCount).to.equal(0);
+ });
+ });
+
+ describe('when a valid connection string is inputted', function () {
+ beforeEach(function () {
+ // Focus the input.
+ userEvent.tab();
+ userEvent.tab();
+ userEvent.tab();
+ userEvent.keyboard('mongodb://localhost');
+ });
+
+ it('should call setConnectionStringUrl with the connection string', function () {
+ expect(setConnectionStringUrlSpy.callCount).to.equal(9);
+ expect(setConnectionStringUrlSpy.lastCall.args[0].toString()).to.equal(
+ 'mongodb://localhost/'
+ );
+ });
+ });
});
describe('the info button', function () {
beforeEach(function () {
- setConnectionStringSpy = sinon.spy();
-
render(
);
});
@@ -103,20 +149,15 @@ describe('ConnectionStringInput Component', function () {
describe('with a connection string', function () {
beforeEach(function () {
- setConnectionStringSpy = sinon.spy();
-
render(
);
});
- afterEach(function () {
- setConnectionStringSpy = null;
- });
-
it('shows the connection string in the text area', function () {
const textArea = screen.getByRole('textbox');
expect(textArea).to.have.text('mongodb+srv://turtles:*****@localhost/');
@@ -157,6 +198,27 @@ describe('ConnectionStringInput Component', function () {
);
});
+ describe('when a valid connection string is inputted', function () {
+ beforeEach(function () {
+ // Focus the input.
+ userEvent.tab();
+ userEvent.tab();
+ userEvent.tab();
+ userEvent.keyboard('?ssl=true');
+ });
+
+ it('should call setConnectionStringUrl with the new connection string', function () {
+ expect(setConnectionStringUrlSpy.callCount).to.equal(9);
+ expect(
+ setConnectionStringUrlSpy.lastCall.args[0].toString()
+ ).to.equal('mongodb+srv://turtles:pineapples@localhost/?ssl=true');
+ });
+
+ it('should not call setConnectionStringError', function () {
+ expect(setConnectionStringErrorSpy.callCount).to.equal(0);
+ });
+ });
+
describe('clicking on edit connection string toggle again', function () {
beforeEach(function () {
// Wait for the modal to close.
diff --git a/packages/connect-form/src/connection-string-input.tsx b/packages/connect-form/src/components/connection-string-input.tsx
similarity index 60%
rename from packages/connect-form/src/connection-string-input.tsx
rename to packages/connect-form/src/components/connection-string-input.tsx
index 60fd05039d6..6be88fa96c6 100644
--- a/packages/connect-form/src/connection-string-input.tsx
+++ b/packages/connect-form/src/components/connection-string-input.tsx
@@ -1,5 +1,12 @@
import { css } from '@emotion/css';
-import React, { ChangeEvent, Fragment, useRef, useReducer } from 'react';
+import React, {
+ ChangeEvent,
+ Fragment,
+ useCallback,
+ useEffect,
+ useReducer,
+ useRef,
+} from 'react';
import {
Icon,
IconButton,
@@ -9,7 +16,9 @@ import {
spacing,
} from '@mongodb-js/compass-components';
import ConfirmEditConnectionString from './confirm-edit-connection-string';
-import { redactConnectionString } from 'mongodb-connection-string-url';
+import ConnectionStringUrl, {
+ redactConnectionString,
+} from 'mongodb-connection-string-url';
const uriLabelStyles = css({
padding: 0,
@@ -24,6 +33,7 @@ const infoButtonStyles = css({
const textAreaContainerStyle = css({
position: 'relative',
+ marginBottom: spacing[2],
});
const connectionStringStyles = css({
@@ -54,19 +64,36 @@ const textAreaLabelContainerStyles = css({
const connectionStringInputId = 'connectionString';
+function connectionStringHasValidScheme(connectionString: string) {
+ return (
+ connectionString.startsWith('mongodb://') ||
+ connectionString.startsWith('mongodb+srv://')
+ );
+}
+
type State = {
+ editingConnectionString: string;
enableEditingConnectionString: boolean;
showConfirmEditConnectionStringPrompt: boolean;
};
type Action =
| { type: 'enable-editing-connection-string' }
+ | {
+ type: 'set-editing-connection-string';
+ editingConnectionString: string;
+ }
| { type: 'show-edit-connection-string-confirmation' }
| { type: 'hide-edit-connection-string-confirmation' }
- | { type: 'hide-connection-string' };
+ | { type: 'disable-editing-connection-string' };
function reducer(state: State, action: Action): State {
switch (action.type) {
+ case 'set-editing-connection-string':
+ return {
+ ...state,
+ editingConnectionString: action.editingConnectionString,
+ };
case 'enable-editing-connection-string':
return {
...state,
@@ -78,7 +105,7 @@ function reducer(state: State, action: Action): State {
...state,
showConfirmEditConnectionStringPrompt: true,
};
- case 'hide-connection-string':
+ case 'disable-editing-connection-string':
return {
...state,
showConfirmEditConnectionStringPrompt: false,
@@ -105,31 +132,90 @@ export function hidePasswordInConnectionString(
function ConnectStringInput({
connectionString,
- setConnectionString,
+ setConnectionStringError,
+ setConnectionStringUrl,
}: {
- connectionString: string;
- setConnectionString: (connectionString: string) => void;
+ connectionString?: string;
+ setConnectionStringError: (errorMessage: string | null) => void;
+ setConnectionStringUrl: (connectionStringUrl: ConnectionStringUrl) => void;
}): React.ReactElement {
+ const textAreaEl = useRef(null);
+
const [
- { enableEditingConnectionString, showConfirmEditConnectionStringPrompt },
+ {
+ editingConnectionString,
+ enableEditingConnectionString,
+ showConfirmEditConnectionStringPrompt,
+ },
dispatch,
] = useReducer(reducer, {
// If there is a connection string default it to protected.
- enableEditingConnectionString: !connectionString,
+ enableEditingConnectionString:
+ !connectionString || connectionString === 'mongodb://localhost:27017/',
showConfirmEditConnectionStringPrompt: false,
+ editingConnectionString: connectionString || '',
});
- const textAreaEl = useRef(null);
+ useEffect(() => {
+ // If the user isn't actively editing the connection string and it
+ // changes (form action) we update the string and disable editing.
+ if (
+ editingConnectionString !== connectionString &&
+ (!textAreaEl.current || textAreaEl.current !== document.activeElement)
+ ) {
+ dispatch({
+ type: 'set-editing-connection-string',
+ editingConnectionString: connectionString || '',
+ });
+ dispatch({
+ type: 'disable-editing-connection-string',
+ });
+ }
+ }, [
+ connectionString,
+ enableEditingConnectionString,
+ editingConnectionString,
+ ]);
+
+ const onChangeConnectionString = useCallback(
+ (event: ChangeEvent) => {
+ const newConnectionString = event.target.value;
+
+ dispatch({
+ type: 'set-editing-connection-string',
+ editingConnectionString: newConnectionString,
+ });
+
+ try {
+ // Ensure it's parsable connection string.
+ const connectionStringUrl = new ConnectionStringUrl(
+ newConnectionString
+ );
+ setConnectionStringUrl(connectionStringUrl);
+ } catch (error) {
+ // Check if starts with url scheme.
+ if (!connectionStringHasValidScheme(newConnectionString)) {
+ setConnectionStringError(
+ 'Invalid schema, expected connection string to start with `mongodb://` or `mongodb+srv://`'
+ );
+ return;
+ }
+
+ setConnectionStringError((error as Error).message);
+ }
+ },
+ [setConnectionStringUrl, setConnectionStringError]
+ );
const displayedConnectionString = enableEditingConnectionString
- ? connectionString
- : hidePasswordInConnectionString(connectionString);
+ ? editingConnectionString
+ : hidePasswordInConnectionString(editingConnectionString);
return (