-
-
- This feature is currently in development.
-
+
+
);
}
+export default connect((state: RootState) => ({
+ shardingStatus: state.status,
+}))(GlobalWrites);
diff --git a/packages/compass-global-writes/src/components/states/sharding.spec.tsx b/packages/compass-global-writes/src/components/states/sharding.spec.tsx
new file mode 100644
index 00000000000..beb6e372928
--- /dev/null
+++ b/packages/compass-global-writes/src/components/states/sharding.spec.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { expect } from 'chai';
+import { screen } from '@mongodb-js/testing-library-compass';
+import { ShardingState } from './sharding';
+import { renderWithStore } from '../../../tests/create-store';
+
+function renderWithProps(
+ props?: Partial>
+) {
+ return renderWithStore();
+}
+
+describe('Sharding', function () {
+ it('renders the info banner', function () {
+ renderWithProps();
+ expect(screen.getByRole('alert')).to.exist;
+ });
+});
diff --git a/packages/compass-global-writes/src/components/states/sharding.tsx b/packages/compass-global-writes/src/components/states/sharding.tsx
new file mode 100644
index 00000000000..f64e788a4ab
--- /dev/null
+++ b/packages/compass-global-writes/src/components/states/sharding.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import {
+ Banner,
+ BannerVariant,
+ Body,
+ css,
+ Link,
+ spacing,
+} from '@mongodb-js/compass-components';
+import { connect } from 'react-redux';
+
+const nbsp = '\u00a0';
+
+const containerStyles = css({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: spacing[400],
+});
+
+export function ShardingState() {
+ return (
+
+
+ Sharding your collection …
+ {nbsp}this should not take too long.
+
+
+ Once your collection is sharded, this tab will show instructions on
+ document ‘location’ field formatting, and provide some common command
+ examples.
+
+
+ You can read more about Global Writes in our documentation.
+
+
+ Configure compound shard key
+
+ To properly configure Global Writes, your collections must be sharded
+ using a compound shard key made up of a ‘location’ field and a second
+ field of your choosing.
+
+
+
+ All documents in your collection should contain both the ‘location’
+ field and your chosen second field.
+
+
+
+
+
+ The second field should represent a well-distributed and immutable
+ value to ensure that data is equally distributed across shards in a
+ particular zone.{nbsp}
+
+ Note that the value of this field cannot be an array.
+
+ {nbsp}
+ For more information, read our documentation on{' '}
+
+ selecting a shard key
+
+ .
+
+
+
+
+
+ Once you shard your collection, it cannot be unsharded.
+
+
+
+
+ To use Global Writes, this collection must be configured with a
+ compound shard key made up of both a ‘location’ field and an
+ identifier field that you should provide.
+
+ {nbsp}See the instructions below for details.
+
+
+
+
+ );
+}
+
+export default connect(
+ (state: RootState) => ({
+ namespace: state.namespace,
+ isSubmittingForSharding:
+ state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING,
+ }),
+ {
+ onCreateShardKey: createShardKey,
+ }
+)(UnshardedState);
diff --git a/packages/compass-global-writes/src/components/states/usharded.spec.tsx b/packages/compass-global-writes/src/components/states/usharded.spec.tsx
new file mode 100644
index 00000000000..a26d5b228de
--- /dev/null
+++ b/packages/compass-global-writes/src/components/states/usharded.spec.tsx
@@ -0,0 +1,193 @@
+import React from 'react';
+import { expect } from 'chai';
+import { screen, userEvent } from '@mongodb-js/testing-library-compass';
+import { UnshardedState } from './unsharded';
+import { renderWithStore } from '../../../tests/create-store';
+import sinon from 'sinon';
+
+function renderWithProps(
+ props?: Partial>
+) {
+ return renderWithStore(
+ {}}
+ {...props}
+ />
+ );
+}
+
+function setShardingKeyFieldValue(value: string) {
+ const input = screen.getByLabelText('Second shard key field');
+ expect(input).to.exist;
+ userEvent.type(input, value);
+ expect(input).to.have.value(value);
+ userEvent.keyboard('{Escape}');
+
+ // For some reason, when running tests in electron mode, the value of
+ // the input field is not being updated. This is a workaround to ensure
+ // the value is being updated before clicking the submit button.
+ userEvent.click(screen.getByText(value), undefined, {
+ skipPointerEventsCheck: true,
+ });
+}
+
+describe('UnshardedState', function () {
+ it('renders the warning banner', function () {
+ renderWithProps();
+ expect(screen.getByRole('alert')).to.exist;
+ });
+
+ it('renders the text to the user', function () {
+ renderWithProps();
+ expect(screen.getByTestId('unsharded-text-description')).to.exist;
+ });
+
+ context('shard collection form', function () {
+ let onCreateShardKeySpy: sinon.SinonSpy;
+ beforeEach(function () {
+ onCreateShardKeySpy = sinon.spy();
+ renderWithProps({ onCreateShardKey: onCreateShardKeySpy });
+ });
+
+ it('renders location form field as disabled', function () {
+ expect(screen.getByLabelText('First shard key field')).to.have.attribute(
+ 'aria-disabled',
+ 'true'
+ );
+ });
+
+ it('does not allow user to submit when no second shard key is selected', function () {
+ expect(screen.getByTestId('shard-collection-button')).to.have.attribute(
+ 'aria-disabled',
+ 'true'
+ );
+
+ userEvent.click(screen.getByTestId('shard-collection-button'));
+ expect(onCreateShardKeySpy.called).to.be.false;
+ });
+
+ it('allows user to input second shard key and submit it', function () {
+ setShardingKeyFieldValue('name');
+
+ userEvent.click(screen.getByTestId('shard-collection-button'));
+
+ expect(onCreateShardKeySpy.calledOnce).to.be.true;
+ expect(onCreateShardKeySpy.firstCall.args[0]).to.deep.equal({
+ customShardKey: 'name',
+ isShardKeyUnique: false,
+ isCustomShardKeyHashed: false,
+ presplitHashedZones: false,
+ numInitialChunks: null,
+ });
+ });
+
+ it('renders advanced options and radio buttons for: default, unique-index and hashed index', function () {
+ const accordian = screen.getByText('Advanced Shard Key Configuration');
+ expect(accordian).to.exist;
+
+ userEvent.click(accordian);
+
+ const defaultRadio = screen.getByLabelText('Default');
+ const uniqueIndexRadio = screen.getByLabelText(
+ 'Use unique index as the shard key'
+ );
+ const hashedIndexRadio = screen.getByLabelText(
+ 'Use hashed index as the shard key'
+ );
+
+ expect(defaultRadio).to.exist;
+ expect(uniqueIndexRadio).to.exist;
+ expect(hashedIndexRadio).to.exist;
+ });
+
+ it('allows user to select unique index as shard key', function () {
+ const accordian = screen.getByText('Advanced Shard Key Configuration');
+ userEvent.click(accordian);
+
+ const uniqueIndexRadio = screen.getByLabelText(
+ 'Use unique index as the shard key'
+ );
+ userEvent.click(uniqueIndexRadio);
+
+ expect(uniqueIndexRadio).to.have.attribute('aria-checked', 'true');
+
+ setShardingKeyFieldValue('name');
+
+ userEvent.click(screen.getByTestId('shard-collection-button'));
+
+ expect(onCreateShardKeySpy.calledOnce).to.be.true;
+ expect(onCreateShardKeySpy.firstCall.args[0]).to.deep.equal({
+ customShardKey: 'name',
+ isShardKeyUnique: true,
+ isCustomShardKeyHashed: false,
+ presplitHashedZones: false,
+ numInitialChunks: null,
+ });
+ });
+
+ it('allows user to select hashed index as shard key with split-chunks option', function () {
+ const accordian = screen.getByText('Advanced Shard Key Configuration');
+ userEvent.click(accordian);
+
+ const hashedIndexRadio = screen.getByLabelText(
+ 'Use hashed index as the shard key'
+ );
+ userEvent.click(hashedIndexRadio);
+
+ expect(hashedIndexRadio).to.have.attribute('aria-checked', 'true');
+
+ setShardingKeyFieldValue('name');
+
+ // Check pre-split data
+ userEvent.click(screen.getByTestId('presplit-data-checkbox'), undefined, {
+ skipPointerEventsCheck: true,
+ });
+
+ userEvent.click(screen.getByTestId('shard-collection-button'));
+
+ expect(onCreateShardKeySpy.calledOnce).to.be.true;
+ expect(onCreateShardKeySpy.firstCall.args[0]).to.deep.equal({
+ customShardKey: 'name',
+ isShardKeyUnique: false,
+ isCustomShardKeyHashed: true,
+ presplitHashedZones: true,
+ numInitialChunks: null,
+ });
+ });
+
+ it('allows user to select hashed index as shard key with all its options', function () {
+ const accordian = screen.getByText('Advanced Shard Key Configuration');
+ userEvent.click(accordian);
+
+ const hashedIndexRadio = screen.getByLabelText(
+ 'Use hashed index as the shard key'
+ );
+ userEvent.click(hashedIndexRadio);
+
+ expect(hashedIndexRadio).to.have.attribute('aria-checked', 'true');
+
+ setShardingKeyFieldValue('name');
+
+ // Check pre-split data
+ userEvent.click(screen.getByTestId('presplit-data-checkbox'), undefined, {
+ skipPointerEventsCheck: true,
+ });
+
+ // Enter number of chunks
+ userEvent.type(screen.getByTestId('chunks-per-shard-input'), '10');
+
+ userEvent.click(screen.getByTestId('shard-collection-button'));
+
+ expect(onCreateShardKeySpy.calledOnce).to.be.true;
+ expect(onCreateShardKeySpy.firstCall.args[0]).to.deep.equal({
+ customShardKey: 'name',
+ isShardKeyUnique: false,
+ isCustomShardKeyHashed: true,
+ presplitHashedZones: true,
+ numInitialChunks: 10,
+ });
+ });
+ });
+});
diff --git a/packages/compass-global-writes/src/index.ts b/packages/compass-global-writes/src/index.ts
index 754e14a2086..a0cadcc0b76 100644
--- a/packages/compass-global-writes/src/index.ts
+++ b/packages/compass-global-writes/src/index.ts
@@ -1,7 +1,7 @@
import React from 'react';
import { registerHadronPlugin } from 'hadron-app-registry';
-import { GlobalWrites } from './components';
+import GlobalWrites from './components';
import { GlobalWritesTabTitle } from './plugin-title';
import { activateGlobalWritesPlugin } from './store';
import { createLoggerLocator } from '@mongodb-js/compass-logging/provider';
@@ -28,6 +28,6 @@ const CompassGlobalWritesHadronPlugin = registerHadronPlugin(
export const CompassGlobalWritesPlugin = {
name: 'GlobalWrites' as const,
provider: CompassGlobalWritesHadronPlugin,
- content: GlobalWrites,
- header: GlobalWritesTabTitle,
+ content: GlobalWrites as React.FunctionComponent,
+ header: GlobalWritesTabTitle as React.FunctionComponent,
};
diff --git a/packages/compass-global-writes/src/plugin-title.tsx b/packages/compass-global-writes/src/plugin-title.tsx
index a733e03dee5..e7fd917d6d2 100644
--- a/packages/compass-global-writes/src/plugin-title.tsx
+++ b/packages/compass-global-writes/src/plugin-title.tsx
@@ -1,5 +1,78 @@
+import { connect } from 'react-redux';
import React from 'react';
+import { type RootState, ShardingStatuses } from './store/reducer';
+import {
+ Body,
+ css,
+ cx,
+ Icon,
+ palette,
+ spacing,
+ Tooltip,
+ useDarkMode,
+} from '@mongodb-js/compass-components';
-export function GlobalWritesTabTitle() {
- return
+ Global Writes{' '}
+ {showWarning && (
+ {
+ // LG does not bubble up the click event to the parent component,
+ // so we add noop onClick and let it bubble up.
+ }}
+ >
+
+
+ }
+ >
+
+ Collections in Atlas Global Clusters with Atlas-managed sharding
+ must be configured with a compound shard key made up of both a
+ 'location' field and an identifier field that you provide.
+ Please configure sharding here.
+
+
+ )}
+