Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions packages/atlas-service/src/atlas-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export type AtlasServiceOptions = {
defaultHeaders?: Record<string, string>;
};

function normalizePath(path?: string) {
path = path ? (path.startsWith('/') ? path : `/${path}`) : '';
return encodeURI(path);
}

export class AtlasService {
private config: AtlasServiceConfig;
constructor(
Expand All @@ -25,16 +30,14 @@ export class AtlasService {
this.config = getAtlasConfig(preferences);
}
adminApiEndpoint(path?: string, requestId?: string): string {
const uri = encodeURI(
`${this.config.atlasApiBaseUrl}${path ? `/${path}` : ''}`
);
const uri = `${this.config.atlasApiBaseUrl}${normalizePath(path)}`;
const query = requestId
? `?request_id=${encodeURIComponent(requestId)}`
: '';
return `${uri}${query}`;
}
cloudEndpoint(path?: string): string {
return encodeURI(`${this.config.cloudBaseUrl}${path ? `/${path}` : ''}`);
return `${this.config.cloudBaseUrl}${normalizePath(path)}`;
}
regionalizedCloudEndpoint(
_atlasMetadata: Pick<AtlasClusterMetadata, 'regionalBaseUrl'>,
Expand All @@ -45,7 +48,7 @@ export class AtlasService {
return this.cloudEndpoint(path);
}
driverProxyEndpoint(path?: string): string {
return encodeURI(`${this.config.wsBaseUrl}${path ? `/${path}` : ''}`);
return `${this.config.wsBaseUrl}${normalizePath(path)}`;
}
async fetch(url: RequestInfo | URL, init?: RequestInit): Promise<Response> {
throwIfNetworkTrafficDisabled(this.preferences);
Expand Down
47 changes: 31 additions & 16 deletions packages/atlas-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,21 @@ export function throwIfNetworkTrafficDisabled(
/**
* https://www.mongodb.com/docs/atlas/api/atlas-admin-api-ref/#errors
*/
export function isServerError(
function isAtlasAPIError(
err: any
): err is { error: number; errorCode: string; detail: string } {
return Boolean(err.error && err.errorCode && err.detail);
return Boolean(err && err.error && err.errorCode && err.detail);
}

function isCloudBackendError(err: any): err is {
errorCode: string;
message: string;
version: string;
status: string;
} {
return Boolean(
err && err.errorCode && err.message && err.version && err.status
);
Comment on lines +49 to +63
Copy link
Collaborator

@syn-zhu syn-zhu Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this second one? i.e. where do you get them from

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I only saw your comment now after I merged. I'm seeing similar code here: https://github.com/10gen/mms/blob/2ee6cb60d064e6eb4d3350d073cba947f1ae26f8/client/packages/components/Access/ServiceAccounts/utils.ts#L223-L232 for example. Don't know if that's related. But I can find results for both message and detail in the codebase.

Hopefully more a question than a review comment :)

}

export async function throwIfNotOk(
Expand All @@ -60,21 +71,25 @@ export async function throwIfNotOk(
}

const messageJSON = await res.json().catch(() => undefined);
if (messageJSON && isServerError(messageJSON)) {
throw new AtlasServiceError(
'ServerError',
res.status,
messageJSON.detail ?? 'Internal server error',
messageJSON.errorCode ?? 'INTERNAL_SERVER_ERROR'
);
} else {
throw new AtlasServiceError(
'NetworkError',
res.status,
res.statusText,
`${res.status}`
);

const status = res.status;
let statusText = res.statusText;
let errorCode = `${res.status}`;
let errorName: 'NetworkError' | 'ServerError' = 'NetworkError';

if (isAtlasAPIError(messageJSON)) {
errorName = 'ServerError';
statusText = messageJSON.detail;
errorCode = messageJSON.errorCode;
}

if (isCloudBackendError(messageJSON)) {
errorName = 'ServerError';
statusText = messageJSON.message;
errorCode = messageJSON.errorCode;
}

throw new AtlasServiceError(errorName, status, statusText, errorCode);
}

export type AtlasServiceConfig = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { OPTIONS, optionChanged } from '../../modules/create-index';

type CheckboxInputProps = {
name: CheckboxOptions;
label: string;
description: string;
label: React.ReactNode;
description: React.ReactNode;
disabled?: boolean;
checked: boolean;
onChange(name: CheckboxOptions, newVal: boolean): void;
Expand All @@ -37,6 +37,9 @@ export const CheckboxInput: React.FunctionComponent<CheckboxInputProps> = ({
onChange(name, event.target.checked);
}}
label={<Label htmlFor={labelId}>{label}</Label>}
// @ts-expect-error leafygreen types only allow strings here, but can
// render a ReactNode too (and we use that to render links inside
// descriptions)
description={description}
disabled={disabled}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { CreateIndexFields } from '../create-index-fields';
import { hasColumnstoreIndexesSupport } from '../../utils/columnstore-indexes';
import CheckboxInput from './checkbox-input';
import CollapsibleInput from './collapsible-input';
import {
useConnectionInfo,
useConnectionSupports,
} from '@mongodb-js/compass-connections/provider';

const createIndexModalFieldsStyles = css({
margin: `${spacing[4]}px 0 ${spacing[5]}px 0`,
Expand Down Expand Up @@ -38,6 +42,11 @@ function CreateIndexForm({
onAddFieldClick,
onRemoveFieldClick,
}: CreateIndexFormProps) {
const { id: connectionId } = useConnectionInfo();
const supportsRollingIndexes = useConnectionSupports(
connectionId,
'rollingIndexCreation'
);
const schemaFields = useAutocompleteFields(namespace);
const schemaFieldNames = useMemo(() => {
return schemaFields
Expand Down Expand Up @@ -86,6 +95,9 @@ function CreateIndexForm({
<CollapsibleInput name="columnstoreProjection"></CollapsibleInput>
)}
<CheckboxInput name="sparse"></CheckboxInput>
{supportsRollingIndexes && (
<CheckboxInput name="buildInRollingProcess"></CheckboxInput>
)}
</div>
</Accordion>
</>
Expand Down
58 changes: 47 additions & 11 deletions packages/compass-indexes/src/modules/create-index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EJSON, ObjectId } from 'bson';
import type { CreateIndexesOptions } from 'mongodb';
import type { CreateIndexesOptions, IndexDirection } from 'mongodb';
import { isCollationValid } from 'mongodb-query-parser';
import React from 'react';
import type { Action, Reducer, Dispatch } from 'redux';
import { Badge } from '@mongodb-js/compass-components';
import { Badge, Link } from '@mongodb-js/compass-components';
import { isAction } from '../utils/is-action';
import type { IndexesThunkAction } from '.';
import type { RootState } from '.';
Expand Down Expand Up @@ -191,6 +191,20 @@ export const OPTIONS = {
description:
'Sparse indexes only contain entries for documents that have the indexed field, even if the index field contains a null value. The index skips over any document that is missing the indexed field.',
},
buildInRollingProcess: {
type: 'checkbox',
label: 'Build in rolling process',
description: (
<>
Building indexes in a rolling fashion can minimize the performance
impact of index builds. We only recommend using rolling index builds
when regular index builds do not meet your needs.{' '}
<Link href="https://www.mongodb.com/docs/manual/core/index-creation/">
Learn More
</Link>
</>
),
},
} as const;

type OptionNames = keyof typeof OPTIONS;
Expand Down Expand Up @@ -317,13 +331,24 @@ function isEmptyValue(value: unknown) {
return false;
}

function fieldTypeToIndexDirection(type: string): IndexDirection {
if (type === '1 (asc)') {
return 1;
}
if (type === '-1 (desc)') {
return -1;
}
if (type === 'text' || type === '2dsphere') {
return type;
}
throw new Error(`Unsupported field type: ${type}`);
}

export const createIndexFormSubmitted = (): IndexesThunkAction<
void,
ErrorEncounteredAction | CreateIndexFormSubmittedAction
> => {
return (dispatch, getState) => {
const spec = {} as CreateIndexSpec;

// Check for field errors.
if (
getState().createIndex.fields.some(
Expand All @@ -336,12 +361,18 @@ export const createIndexFormSubmitted = (): IndexesThunkAction<

const formIndexOptions = getState().createIndex.options;

getState().createIndex.fields.forEach((field: Field) => {
let type: string | number = field.type;
if (field.type === '1 (asc)') type = 1;
if (field.type === '-1 (desc)') type = -1;
spec[field.name] = type;
});
let spec: Record<string, IndexDirection>;

try {
spec = Object.fromEntries(
getState().createIndex.fields.map((field) => {
return [field.name, fieldTypeToIndexDirection(field.type)];
})
);
} catch (e) {
dispatch(errorEncountered((e as any).message));
return;
}

const options: CreateIndexesOptions = {};

Expand Down Expand Up @@ -437,7 +468,12 @@ export const createIndexFormSubmitted = (): IndexesThunkAction<

dispatch({ type: ActionTypes.CreateIndexFormSubmitted });
void dispatch(
createRegularIndex(getState().createIndex.indexId, spec, options)
createRegularIndex(
getState().createIndex.indexId,
spec,
options,
!!formIndexOptions.buildInRollingProcess.value
)
);
};
};
Expand Down
14 changes: 9 additions & 5 deletions packages/compass-indexes/src/modules/regular-indexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
hideModalDescription,
unhideModalDescription,
} from '../utils/modal-descriptions';
import type { IndexSpecification, CreateIndexesOptions } from 'mongodb';
import type { CreateIndexesOptions, IndexDirection } from 'mongodb';
import { hasColumnstoreIndex } from '../utils/columnstore-indexes';

export type RegularIndex = Partial<IndexDefinition> &
Expand Down Expand Up @@ -428,8 +428,9 @@ const indexCreationFailed = (

export function createRegularIndex(
inProgressIndexId: string,
spec: CreateIndexSpec,
options: CreateIndexesOptions
spec: Record<string, IndexDirection>,
options: CreateIndexesOptions,
isRollingIndexBuild: boolean
): IndexesThunkAction<
Promise<void>,
| IndexCreationStartedAction
Expand All @@ -439,7 +440,7 @@ export function createRegularIndex(
return async (
dispatch,
getState,
{ track, dataService, connectionInfoRef }
{ track, dataService, rollingIndexesService, connectionInfoRef }
) => {
const ns = getState().namespace;
const inProgressIndex = prepareInProgressIndex(inProgressIndexId, {
Expand Down Expand Up @@ -471,7 +472,10 @@ export function createRegularIndex(
};

try {
await dataService.createIndex(ns, spec as IndexSpecification, options);
const createFn = isRollingIndexBuild
? rollingIndexesService.createRollingIndex.bind(rollingIndexesService)
: dataService.createIndex.bind(dataService);
await createFn(ns, spec, options);
dispatch(indexCreationSucceeded(inProgressIndexId));
track('Index Created', trackEvent, connectionInfoRef.current);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ describe('RollingIndexesService', function () {
const atlasServiceStub = {
automationAgentRequest: Sinon.stub(),
automationAgentAwait: Sinon.stub(),
authenticatedFetch: Sinon.stub(),
cloudEndpoint: Sinon.stub().callsFake((str) => str),
};
let service: RollingIndexesService;

Expand Down Expand Up @@ -55,15 +57,19 @@ describe('RollingIndexesService', function () {
});

describe('createRollingIndex', function () {
it('should fail if automation agent returned unexpected result', async function () {
atlasServiceStub.automationAgentRequest.resolves({ _id: '_id' });
it('should send the request to the kinda automation agent endpoint with the matching body and path params', async function () {
await service.createRollingIndex('db.coll', {}, {});

try {
await service.createRollingIndex('db.coll', {}, {});
expect.fail('expected createRollingIndex to throw');
} catch (err) {
expect(err).not.to.be.null;
}
expect(atlasServiceStub.authenticatedFetch).to.have.been.calledOnce;

const { args } = atlasServiceStub.authenticatedFetch.getCall(0);

expect(args[0]).to.eq('/explorer/v1/groups/abc/clusters/123/index');
expect(args[1]).to.have.property('method', 'POST');
expect(args[1]).to.have.property(
'body',
'{"clusterId":"123","db":"db","collection":"coll","keys":"{}","options":"","collationOptions":""}'
);
});
});
});
Loading