diff --git a/package-lock.json b/package-lock.json
index 9982ddc9199..f7d3d8ca1cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5576,6 +5576,52 @@
"node": ">=10"
}
},
+ "node_modules/@leafygreen-ui/radio-group": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/radio-group/-/radio-group-7.0.6.tgz",
+ "integrity": "sha512-LsokgU+fX2b9teMeiE0VPNH6K9RsGOwesmiZr40q3Df+LWVTMqsjWqIMhXWe1gAwgqqHewdkaXb4eLCLG3rs2w==",
+ "dependencies": {
+ "@leafygreen-ui/hooks": "^7.0.0",
+ "@leafygreen-ui/interaction-ring": "^1.1.0",
+ "@leafygreen-ui/lib": "^9.0.0",
+ "@leafygreen-ui/palette": "^3.2.2"
+ }
+ },
+ "node_modules/@leafygreen-ui/radio-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-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-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",
@@ -63585,6 +63631,7 @@
"@leafygreen-ui/palette": "^3.2.2",
"@leafygreen-ui/portal": "^3.1.3",
"@leafygreen-ui/radio-box-group": "^6.1.4",
+ "@leafygreen-ui/radio-group": "^7.0.6",
"@leafygreen-ui/segmented-control": "^0.9.1",
"@leafygreen-ui/select": "^3.0.8",
"@leafygreen-ui/table": "^2.0.5",
@@ -94183,17 +94230,6 @@
"decamelize": "^1.2.0"
}
},
- "packages/compass/node_modules/@mongosh/node-runtime-worker-thread": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@mongosh/node-runtime-worker-thread/-/node-runtime-worker-thread-1.1.9.tgz",
- "integrity": "sha512-wbct5QHogcVsPmIz+thXe6CTw7GnG53OtPDumwY63TKsxCJjrcycCjwE2+xXommAhqgv3KkYxj1DPvBlsS8KXA==",
- "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",
@@ -108753,6 +108789,48 @@
}
}
},
+ "@leafygreen-ui/radio-group": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/@leafygreen-ui/radio-group/-/radio-group-7.0.6.tgz",
+ "integrity": "sha512-LsokgU+fX2b9teMeiE0VPNH6K9RsGOwesmiZr40q3Df+LWVTMqsjWqIMhXWe1gAwgqqHewdkaXb4eLCLG3rs2w==",
+ "requires": {
+ "@leafygreen-ui/hooks": "^7.0.0",
+ "@leafygreen-ui/interaction-ring": "^1.1.0",
+ "@leafygreen-ui/lib": "^9.0.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",
@@ -114689,6 +114767,7 @@
"@leafygreen-ui/palette": "^3.2.2",
"@leafygreen-ui/portal": "^3.1.3",
"@leafygreen-ui/radio-box-group": "^6.1.4",
+ "@leafygreen-ui/radio-group": "^7.0.6",
"@leafygreen-ui/segmented-control": "^0.9.1",
"@leafygreen-ui/select": "^3.0.8",
"@leafygreen-ui/table": "^2.0.5",
@@ -172632,14 +172711,6 @@
"web-vitals": "^2.1.2"
},
"dependencies": {
- "@mongosh/node-runtime-worker-thread": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@mongosh/node-runtime-worker-thread/-/node-runtime-worker-thread-1.1.9.tgz",
- "integrity": "sha512-wbct5QHogcVsPmIz+thXe6CTw7GnG53OtPDumwY63TKsxCJjrcycCjwE2+xXommAhqgv3KkYxj1DPvBlsS8KXA==",
- "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 2d829db9cb1..cf202fb38db 100644
--- a/packages/compass-components/package.json
+++ b/packages/compass-components/package.json
@@ -52,6 +52,7 @@
"@leafygreen-ui/palette": "^3.2.2",
"@leafygreen-ui/portal": "^3.1.3",
"@leafygreen-ui/radio-box-group": "^6.1.4",
+ "@leafygreen-ui/radio-group": "^7.0.6",
"@leafygreen-ui/segmented-control": "^0.9.1",
"@leafygreen-ui/select": "^3.0.8",
"@leafygreen-ui/table": "^2.0.5",
diff --git a/packages/compass-components/src/components/file-input.spec.tsx b/packages/compass-components/src/components/file-input.spec.tsx
index f91576f650e..3dcc36bbc1b 100644
--- a/packages/compass-components/src/components/file-input.spec.tsx
+++ b/packages/compass-components/src/components/file-input.spec.tsx
@@ -2,7 +2,13 @@ import React from 'react';
import sinon from 'sinon';
import { expect } from 'chai';
-import { render, screen, cleanup } from '@testing-library/react';
+import {
+ render,
+ screen,
+ cleanup,
+ fireEvent,
+ waitFor,
+} from '@testing-library/react';
import FileInput, { Variant } from './file-input';
@@ -182,4 +188,126 @@ describe('FileInput', function () {
const errorMessage = screen.getByTestId('file-input-error');
expect(errorMessage).to.exist;
});
+
+ it('does not show optional if not specified', function () {
+ render(
+
+ );
+
+ expect(screen.queryByText('Optional')).to.equal(null);
+ });
+
+ it('renders the optional when specified', function () {
+ render(
+
+ );
+
+ expect(screen.getByText('Optional')).to.be.visible;
+ });
+
+ it('renders the optional message when specified', function () {
+ render(
+
+ );
+
+ expect(screen.getByText('pineapples')).to.be.visible;
+ });
+
+ describe('when a file is chosen', function () {
+ beforeEach(async function () {
+ render(
+
+ );
+
+ const fileInput = screen.getByTestId('test-file-input');
+
+ await waitFor(() =>
+ fireEvent.change(fileInput, {
+ target: {
+ files: [
+ {
+ path: 'new/file/path',
+ },
+ ],
+ },
+ })
+ );
+ });
+
+ it('calls onChange with the chosen file', function () {
+ expect(spy.callCount).to.equal(1);
+ expect(spy.firstCall.args[0]).to.deep.equal(['new/file/path']);
+ });
+ });
+
+ it('renders the file name with close button when showFileOnNewLine=true', function () {
+ render(
+
+ );
+
+ expect(screen.getByText('new/file/nice')).to.be.visible;
+ expect(screen.getByText('another/file/path')).to.be.visible;
+ expect(screen.getAllByLabelText('Remove file').length).to.equal(2);
+ });
+
+ describe('when a file is clicked to remove on multi line', function () {
+ beforeEach(async function () {
+ render(
+
+ );
+
+ const removeButton = screen.getAllByLabelText('Remove file')[0];
+
+ await waitFor(() => fireEvent.click(removeButton));
+ });
+
+ it('calls onChange with the file removed', function () {
+ expect(spy.callCount).to.equal(1);
+ expect(spy.firstCall.args[0]).to.deep.equal(['another/file/path']);
+ });
+ });
});
diff --git a/packages/compass-components/src/components/file-input.tsx b/packages/compass-components/src/components/file-input.tsx
index cab5ad3509d..94180a3c006 100644
--- a/packages/compass-components/src/components/file-input.tsx
+++ b/packages/compass-components/src/components/file-input.tsx
@@ -4,7 +4,7 @@ import { css, cx } from '@leafygreen-ui/emotion';
import { uiColors } from '@leafygreen-ui/palette';
import { spacing } from '@leafygreen-ui/tokens';
-import { Button, Icon, Label, Link, Description } from '..';
+import { Button, Icon, IconButton, Label, Link, Description } from '..';
const { base: redBaseColor } = uiColors.red;
@@ -14,16 +14,24 @@ const formItemHorizontalStyles = css({
marginRight: 'auto',
marginLeft: 'auto',
display: 'flex',
- alignItems: 'center',
});
-const formItemVerticalStyles = css`
- margin: 5px auto 20px;
-`;
+const formItemVerticalStyles = css({
+ margin: '5px auto 20px',
+});
-const buttonStyles = css`
- width: 100%;
-`;
+const removeFileLineStyles = css({
+ display: 'flex',
+ flexDirection: 'row',
+});
+
+const removeFileButtonStyles = css({
+ marginLeft: spacing[1],
+});
+
+const buttonStyles = css({
+ width: '100%',
+});
const errorMessageStyles = css({
color: `${redBaseColor} !important`,
@@ -39,31 +47,45 @@ const labelHorizontalStyles = css({
paddingRight: spacing[3],
});
-const labelIconStyles = css`
- display: inline-block;
- vertical-align: middle;
- font: normal normal normal 14px/1 FontAwesome;
- font-size: inherit;
- text-rendering: auto;
- margin: 0 0 0 5px;
- cursor: pointer;
- color: #bfbfbe;
-
- &:link,
- &:active {
- color: #bfbfbe;
- }
-
- &:link,
- &:active,
- &:hover {
- text-decoration: none;
- }
-
- &:hover {
- color: #fbb129;
- }
-`;
+const optionalLabelStyles = css({
+ color: uiColors.gray.base,
+ fontStyle: 'italic',
+ fontWeight: 'normal',
+ fontSize: 12,
+});
+
+const labelIconStyles = css({
+ display: 'inline-block',
+ verticalAlign: 'middle',
+ font: 'normal normal normal 14px/1 FontAwesome',
+ fontSize: 'inherit',
+ textRendering: 'auto',
+ margin: '0 0 0 5px',
+ cursor: 'pointer',
+ color: '#bfbfbe',
+
+ '&:link, &:active': {
+ color: '#bfbfbe',
+ },
+
+ '&:link, &:active, &:hover': {
+ textDecoration: 'none',
+ },
+
+ '&:hover': {
+ color: '#fbb129',
+ },
+});
+
+const disabledLabelStyles = css({
+ '&:first-child': {
+ pointerEvents: 'none',
+ },
+});
+
+const disabledDescriptionStyles = css({
+ color: uiColors.gray.dark1,
+});
export enum Variant {
Horizontal = 'HORIZONTAL',
@@ -81,10 +103,14 @@ function FileInput({
label,
dataTestId,
onChange,
+ disabled,
multi = false,
+ optional = false,
+ optionalMessage,
error = false,
errorMessage,
variant = Variant.Horizontal,
+ showFileOnNewLine = false,
link,
description,
values,
@@ -92,14 +118,18 @@ function FileInput({
}: {
id: string;
label: string;
- dataTestId: string;
+ dataTestId?: string;
onChange: (files: string[]) => void;
+ disabled?: boolean;
multi?: boolean;
+ optional?: boolean;
+ optionalMessage?: string;
error?: boolean;
errorMessage?: string;
variant?: Variant;
link?: string;
description?: string;
+ showFileOnNewLine?: boolean;
values?: string[];
labelAlignment?: 'right' | 'left' | 'center';
}): React.ReactElement {
@@ -124,9 +154,9 @@ function FileInput({
[onChange]
);
- const renderDescription = () => {
+ const renderDescription = (): React.ReactElement | null => {
if (!link && !description) {
- return <>>;
+ return null;
}
if (!link) {
return (
@@ -165,18 +195,40 @@ function FileInput({
{ [formItemVerticalStyles]: variant === Variant.Vertical }
)}
>
-
+
+ {showFileOnNewLine &&
+ values &&
+ values.length > 0 &&
+ values.map((value, index) => (
+
+ {value}
+ {
+ const newValues = [...values];
+ newValues.splice(index, 1);
+ onChange(newValues);
+ }}
+ >
+
+
+
+ ))}
{error && errorMessage && (
-
+
{isSRV ? 'Hostname' : 'Host'}
diff --git a/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-tab.tsx
index efd0a7d1be6..ccbe7efae46 100644
--- a/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-tab.tsx
+++ b/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-tab.tsx
@@ -63,6 +63,7 @@ const containerStyles = css({
const contentStyles = css({
marginTop: spacing[3],
width: '50%',
+ minWidth: 400,
});
function SSHTunnel({
diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-certificate-authority.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-certificate-authority.tsx
new file mode 100644
index 00000000000..00cab9946a7
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-certificate-authority.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { css, FileInput } from '@mongodb-js/compass-components';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+import { MongoClientOptions } from 'mongodb';
+
+import FormFieldContainer from '../../form-field-container';
+
+const caFieldsContainer = css({
+ width: '80%',
+});
+
+function TLSCertificateAuthority({
+ connectionStringUrl,
+ disabled,
+ updateCAFile,
+}: {
+ connectionStringUrl: ConnectionStringUrl;
+ disabled: boolean;
+ updateCAFile: (newCAFile: string | null) => void;
+}): React.ReactElement {
+ const caFile = connectionStringUrl
+ .typedSearchParams()
+ .get('tlsCAFile');
+
+ return (
+
+ {
+ updateCAFile(files && files.length > 0 ? files[0] : null);
+ }}
+ showFileOnNewLine
+ values={caFile ? [caFile] : undefined}
+ optional
+ />
+
+ );
+}
+
+export default TLSCertificateAuthority;
diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-client-certificate.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-client-certificate.tsx
new file mode 100644
index 00000000000..2c49a35e4d4
--- /dev/null
+++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-client-certificate.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { FileInput, TextInput, css } from '@mongodb-js/compass-components';
+import ConnectionStringUrl from 'mongodb-connection-string-url';
+import { MongoClientOptions } from 'mongodb';
+
+import FormFieldContainer from '../../form-field-container';
+
+const inputFieldStyles = css({
+ width: '80%',
+});
+
+function TLSClientCertificate({
+ connectionStringUrl,
+ disabled,
+ updateTLSClientCertificate,
+ updateTLSClientCertificatePassword,
+}: {
+ connectionStringUrl: ConnectionStringUrl;
+ disabled: boolean;
+ updateTLSClientCertificate: (
+ newClientCertificateKeyFile: string | null
+ ) => void;
+ updateTLSClientCertificatePassword: (newPassword: string | null) => void;
+}): React.ReactElement {
+ const typedParams =
+ connectionStringUrl.typedSearchParams();
+ const clientCertificateKeyFile = typedParams.get('tlsCertificateKeyFile');
+ const tlsCertificateKeyFilePassword = typedParams.get(
+ 'tlsCertificateKeyFilePassword'
+ );
+
+ return (
+ <>
+
+ {
+ updateTLSClientCertificate(
+ files && files.length > 0 ? files[0] : null
+ );
+ }}
+ showFileOnNewLine
+ optional
+ optionalMessage="Optional (only required with X.509)"
+ />
+
+
+ ) => {
+ updateTLSClientCertificatePassword(value);
+ }}
+ disabled={disabled}
+ data-testid="tlsCertificateKeyFilePassword-input"
+ id="tlsCertificateKeyFilePassword"
+ label="Client Key Password"
+ type="password"
+ value={tlsCertificateKeyFilePassword || ''}
+ optional
+ />
+
+ >
+ );
+}
+
+export default TLSClientCertificate;
diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx
index d5f7bec51ee..3e1bf3b59b4 100644
--- a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx
+++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { expect } from 'chai';
import sinon from 'sinon';
import ConnectionStringUrl from 'mongodb-connection-string-url';
@@ -14,16 +14,21 @@ describe('SchemaInput', function () {
});
describe('with ssl=true', function () {
+ let testUrl: ConnectionStringUrl;
+ let rerender: (
+ ui: React.ReactElement>
+ ) => void;
beforeEach(function () {
- const connectionStringUrl = new ConnectionStringUrl(
+ testUrl = new ConnectionStringUrl(
'mongodb+srv://0ranges:p!neapp1es@localhost/?ssl=true'
);
- render(
+ const component = render(
);
+ rerender = component.rerender;
});
it('should render the TLS/SSL `On` radio box selected', function () {
@@ -32,6 +37,11 @@ describe('SchemaInput', function () {
expect(tlsOnRadioBox.getAttribute('aria-checked')).to.equal('true');
});
+ it('should render the client cert and CA file labels', function () {
+ expect(screen.getByText('Certificate Authority (.pem)')).to.be.visible;
+ expect(screen.getByText('Client Certificate (.pem)')).to.be.visible;
+ });
+
it('should render TLS/SSL `Default` and `Off` radio boxes not selected', function () {
const tlsDefaultRadioBox = screen.getAllByRole(
'radio'
@@ -46,6 +56,14 @@ describe('SchemaInput', function () {
expect(tlsOffRadioBox.getAttribute('aria-checked')).to.equal('false');
});
+ it('should render all of the checkboxes unchecked', function () {
+ const checkboxes: HTMLInputElement[] = screen.getAllByRole('checkbox');
+ expect(checkboxes.length).to.equal(3);
+ expect(checkboxes.find((checkbox) => checkbox.checked)).to.equal(
+ undefined
+ );
+ });
+
describe('when TLS/SSL default is clicked', function () {
beforeEach(function () {
const tlsDefaultRadioBox = screen.getAllByRole('radio')[0];
@@ -86,6 +104,192 @@ describe('SchemaInput', function () {
expect(updateConnectionFormFieldSpy.callCount).to.equal(0);
});
});
+
+ describe('when a tlsCAFile is chosen', function () {
+ beforeEach(async function () {
+ const fileInput = screen.getByTestId('tlsCAFile-input');
+
+ await waitFor(() =>
+ fireEvent.change(fileInput, {
+ target: {
+ files: [
+ {
+ path: 'new/caFile/path',
+ },
+ ],
+ },
+ })
+ );
+ });
+
+ it('should call to update the tlsCAFile with the chosen file', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-search-param',
+ currentKey: 'tlsCAFile',
+ value: 'new/caFile/path',
+ });
+ });
+ });
+
+ describe('when a tlsCertificateKeyFile is chosen', function () {
+ beforeEach(async function () {
+ const fileInput = screen.getByTestId('tlsCertificateKeyFile-input');
+
+ await waitFor(() =>
+ fireEvent.change(fileInput, {
+ target: {
+ files: [
+ {
+ path: 'new/caFile/path',
+ },
+ ],
+ },
+ })
+ );
+ });
+
+ it('should call to update the tlsCertificateKeyFile with the chosen file', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({
+ type: 'update-search-param',
+ currentKey: 'tlsCertificateKeyFile',
+ value: 'new/caFile/path',
+ });
+ });
+ });
+
+ describe('when tlsCAFile exists', function () {
+ beforeEach(function () {
+ testUrl.searchParams.set('tlsCAFile', 'pineapples');
+ rerender(
+
+ );
+ });
+
+ it('should render the filepath', function () {
+ expect(screen.getAllByText('pineapples').length).to.equal(2);
+ });
+ });
+
+ describe('when tlsCertificateKeyFile exists', function () {
+ beforeEach(function () {
+ testUrl.searchParams.set('tlsCertificateKeyFile', 'a_great_file_path');
+ rerender(
+
+ );
+ });
+
+ it('should render the filepath', function () {
+ expect(screen.getAllByText('a_great_file_path').length).to.equal(2);
+ });
+ });
+
+ describe('when tlsCertificateKeyFilePassword exists', function () {
+ beforeEach(function () {
+ testUrl.searchParams.set(
+ 'tlsCertificateKeyFilePassword',
+ 'tlsClientPassword'
+ );
+ rerender(
+
+ );
+ });
+
+ it('should render the password', function () {
+ expect(
+ screen
+ .getByTestId('tlsCertificateKeyFilePassword-input')
+ .getAttribute('type')
+ ).to.equal('password');
+ expect(screen.getByTestId('tlsCertificateKeyFilePassword-input')).to.be
+ .visible;
+ expect(
+ screen
+ .getByTestId('tlsCertificateKeyFilePassword-input')
+ .getAttribute('value')
+ ).to.equal('tlsClientPassword');
+ });
+ });
+
+ // eslint-disable-next-line mocha/no-setup-in-describe
+ [
+ 'tlsInsecure',
+ 'tlsAllowInvalidHostnames',
+ 'tlsAllowInvalidCertificates',
+ ].forEach((connectionStringTlsParam) => {
+ describe('with ', function () {
+ it('should render the checkbox not checked', function () {
+ const checkbox: HTMLInputElement = screen.getByTestId(
+ `${connectionStringTlsParam}-input`
+ );
+ expect(checkbox.checked).to.equal(false);
+ });
+
+ describe(`when ${connectionStringTlsParam} is clicked`, function () {
+ beforeEach(function () {
+ const checkboxLabel = screen.getByText(connectionStringTlsParam);
+ fireEvent.click(checkboxLabel);
+ });
+
+ it('should call to update the connection configuration to set the param to true', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(
+ updateConnectionFormFieldSpy.firstCall.args[0]
+ ).to.deep.equal({
+ type: 'update-search-param',
+ currentKey: connectionStringTlsParam,
+ value: true,
+ });
+ });
+ });
+
+ describe(`when ${connectionStringTlsParam} is true`, function () {
+ beforeEach(function () {
+ testUrl.searchParams.set(connectionStringTlsParam, 'true');
+ rerender(
+
+ );
+ });
+
+ it('should render the checkbox checked', function () {
+ const checkbox: HTMLInputElement = screen.getByTestId(
+ `${connectionStringTlsParam}-input`
+ );
+ expect(checkbox.checked).to.equal(true);
+ });
+
+ describe(`when ${connectionStringTlsParam} is clicked`, function () {
+ beforeEach(function () {
+ const checkboxLabel = screen.getByText(connectionStringTlsParam);
+ fireEvent.click(checkboxLabel);
+ });
+
+ it('should call to update the connection configuration to set the param to false', function () {
+ expect(updateConnectionFormFieldSpy.callCount).to.equal(1);
+ expect(
+ updateConnectionFormFieldSpy.firstCall.args[0]
+ ).to.deep.equal({
+ type: 'delete-search-param',
+ key: connectionStringTlsParam,
+ });
+ });
+ });
+ });
+ });
+ });
});
describe('with ssl=false', function () {
@@ -107,6 +311,13 @@ describe('SchemaInput', function () {
expect(tlsOnRadioBox.getAttribute('aria-checked')).to.equal('true');
});
+ it('should render all of the checkboxes disabled', function () {
+ const checkboxes: HTMLInputElement[] = screen.getAllByRole('checkbox');
+ expect(checkboxes.find((checkbox) => !checkbox.disabled)).to.equal(
+ undefined
+ );
+ });
+
describe('when TLS/SSL off is clicked', function () {
beforeEach(function () {
const standardSchemaRadioBox = screen.getAllByRole('radio')[2];
diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx
index ee037314375..86762310773 100644
--- a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx
+++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx
@@ -1,23 +1,40 @@
import React, { useCallback } from 'react';
import {
+ Checkbox,
Description,
+ Icon,
+ IconButton,
Label,
RadioBox,
RadioBoxGroup,
spacing,
+ uiColors,
css,
+ cx,
} from '@mongodb-js/compass-components';
import ConnectionStringUrl from 'mongodb-connection-string-url';
import type { MongoClientOptions } from 'mongodb';
import { UpdateConnectionFormField } from '../../../hooks/use-connect-form';
import FormFieldContainer from '../../form-field-container';
+import TLSClientCertificate from './tls-client-certificate';
+import TLSCertificateAuthority from './tls-certificate-authority';
import { TLS_OPTIONS } from '../../../utils/tls-options';
-const descriptionStyles = css({
+const tlsDescriptionStyles = css({
marginTop: spacing[1],
});
+const infoButtonStyles = css({
+ verticalAlign: 'middle',
+ marginTop: -spacing[2],
+ marginBottom: -spacing[2],
+});
+
+const disabledCheckboxDescriptionStyles = css({
+ color: uiColors.gray.light1,
+});
+
const TLS_TYPES: {
value: TLS_OPTIONS;
label: string;
@@ -96,11 +113,67 @@ function TLSTab({
[updateConnectionFormField]
);
+ const tlsOptionFields: {
+ name: keyof MongoClientOptions;
+ description: string;
+ checked: boolean;
+ }[] = [
+ {
+ name: 'tlsInsecure',
+ description:
+ 'This includes tlsAllowInvalidHostnames and tlsAllowInvalidCertificates. This is not recommended as disabling certificate validation creates a vulnerability.',
+ checked: connectionStringUrl.searchParams.get('tlsInsecure') === 'true',
+ },
+ {
+ name: 'tlsAllowInvalidHostnames',
+ description:
+ 'This disables the validation of the hostnames in the certificate presented by the mongod/mongos instance.',
+ checked:
+ connectionStringUrl.searchParams.get('tlsAllowInvalidHostnames') ===
+ 'true',
+ },
+ {
+ name: 'tlsAllowInvalidCertificates',
+ description:
+ 'This disables validating the server certificates. This is not recommended as it creates a vulnerability to expired mongod and mongos certificates as well as to foreign processes posing as valid mongod or mongos instances.',
+ checked:
+ connectionStringUrl.searchParams.get('tlsAllowInvalidCertificates') ===
+ 'true',
+ },
+ ];
+
+ const tlsOptionsDisabled = tlsOption !== 'ON';
+
+ const handleFieldChanged = useCallback(
+ (key: keyof MongoClientOptions, value: unknown) => {
+ if (!value) {
+ return updateConnectionFormField({
+ type: 'delete-search-param',
+ key,
+ });
+ }
+ return updateConnectionFormField({
+ type: 'update-search-param',
+ currentKey: key,
+ value,
+ });
+ },
+ [updateConnectionFormField]
+ );
+
return (
SSL/TLS Connection
+
+
+
{TLS_TYPES.map((tlsType) => (
@@ -109,11 +182,54 @@ function TLSTab({
))}
-
+
{TLS_TYPES.find((tlsType) => tlsType.value === tlsOption)
?.description || ''}
+ {
+ handleFieldChanged('tlsCAFile', newCertificatePath);
+ }}
+ />
+ {
+ handleFieldChanged('tlsCertificateKeyFile', newCertificatePath);
+ }}
+ updateTLSClientCertificatePassword={(
+ newCertificatePath: string | null
+ ) => {
+ handleFieldChanged(
+ 'tlsCertificateKeyFilePassword',
+ newCertificatePath
+ );
+ }}
+ />
+ {tlsOptionFields.map((tlsOptionField) => (
+
+ ) => {
+ handleFieldChanged(tlsOptionField.name, event.target.checked);
+ }}
+ data-testid={`${tlsOptionField.name}-input`}
+ label={tlsOptionField.name}
+ disabled={tlsOptionsDisabled}
+ checked={tlsOptionField.checked}
+ bold={false}
+ />
+
+ {tlsOptionField.description}
+
+
+ ))}
);
}
diff --git a/packages/connect-form/src/components/connect-form.tsx b/packages/connect-form/src/components/connect-form.tsx
index 2af20715224..3d0f9b728cc 100644
--- a/packages/connect-form/src/components/connect-form.tsx
+++ b/packages/connect-form/src/components/connect-form.tsx
@@ -21,9 +21,7 @@ const formContainerStyles = css({
margin: 0,
padding: 0,
height: 'fit-content',
- flexGrow: 1,
- minWidth: 650,
- maxWidth: 760,
+ width: 700,
position: 'relative',
display: 'inline-block',
});
diff --git a/packages/connect-form/src/components/form-field-container.tsx b/packages/connect-form/src/components/form-field-container.tsx
index 76426938e5b..e333f0563bc 100644
--- a/packages/connect-form/src/components/form-field-container.tsx
+++ b/packages/connect-form/src/components/form-field-container.tsx
@@ -1,4 +1,4 @@
-import { spacing, css } from '@mongodb-js/compass-components';
+import { spacing, css, cx } from '@mongodb-js/compass-components';
import React from 'react';
const formFieldContainerStyles = css({
@@ -6,11 +6,15 @@ const formFieldContainerStyles = css({
});
function FormFieldContainer({
+ className = '',
children,
}: {
+ className?: string;
children: React.ReactNode;
}): React.ReactElement {
- return {children}
;
+ return (
+ {children}
+ );
}
export default FormFieldContainer;