Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import {
fireEvent,
render,
screen,
within,
} from '@mongodb-js/testing-library-compass';
import { expect } from 'chai';
import type { Stage } from './stage-operator-select';
import { StageOperatorSelect } from './stage-operator-select';
import Sinon from 'sinon';

describe('StageOperatorSelect', () => {
const mockStages: Stage[] = [
{
name: 'basicStage',
env: ['on-prem'],
description: 'basicStage description.',
},
{
name: 'atlasOnlyStage',
env: ['atlas'],
description: 'atlasOnlyStage description.',
},
{
name: '$search',
env: ['atlas'],
description: 'searchStage description.',
},
];

const defaultMockProps = {
index: 0,
onChange: Sinon.stub(),
selectedStage: null,
isDisabled: false,
stages: mockStages,
serverVersion: '8.1.0',
isReadonlyView: false,
collectionStats: {
pipeline: [{ $addFields: { field: 'value' } }],
},
};

const renderCombobox = (
props: Partial<React.ComponentProps<typeof StageOperatorSelect>> = {}
) => {
return render(<StageOperatorSelect {...defaultMockProps} {...props} />);
};

it('renders the correct descriptions if not in readonly view', () => {
renderCombobox({ isReadonlyView: false });
fireEvent.click(screen.getByRole('combobox'));
const listbox = screen.getByRole('listbox');

expect(within(listbox).getByText('basicStage description.')).to.exist;
expect(within(listbox).getByText('Atlas only. atlasOnlyStage description.'))
.to.exist;
expect(within(listbox).getByText('Atlas only. searchStage description.')).to
.exist;
});

it('renders the correct descriptions if in readonly view with non queryable pipeline', () => {
renderCombobox({
isReadonlyView: true,
collectionStats: {
pipeline: [
{ $addFields: { field: 'value' } },
{ project: { newField: 1 } },
],
},
});
fireEvent.click(screen.getByRole('combobox'));
const listbox = screen.getByRole('listbox');

expect(within(listbox).getByText('basicStage description.')).to.exist;
expect(within(listbox).getByText('Atlas only. atlasOnlyStage description.'))
.to.exist;
expect(
within(listbox).getByText(
'Atlas only. Only views containing $addFields, $set or $match stages with the $expr operator are compatible with search indexes. searchStage description.'
)
).to.exist;
});

it('renders the correct descriptions for $search stage in readonly view with incompatible version', () => {
renderCombobox({ serverVersion: '7.0.0', isReadonlyView: true });
fireEvent.click(screen.getByRole('combobox'));
const listbox = screen.getByRole('listbox');

expect(within(listbox).getByText('basicStage description.')).to.exist;
expect(within(listbox).getByText('Atlas only. atlasOnlyStage description.'))
.to.exist;
expect(
within(listbox).getByText(
'Atlas only. Requires MongoDB 8.1+ to run on a view. searchStage description.'
)
).to.exist;
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { useCallback } from 'react';
import { withPreferences } from 'compass-preferences-model/provider';
import {
usePreference,
withPreferences,
} from 'compass-preferences-model/provider';
import { connect } from 'react-redux';
import { VIEW_PIPELINE_UTILS } from '@mongodb-js/mongodb-constants';

import {
Combobox,
Expand All @@ -13,9 +17,10 @@ import type { RootState } from '../../modules';
import { changeStageOperator } from '../../modules/pipeline-builder/stage-editor';
import type { StoreStage } from '../../modules/pipeline-builder/stage-editor';

import { filterStageOperators } from '../../utils/stage';
import { filterStageOperators, isSearchStage } from '../../utils/stage';
import { isAtlasOnly } from '../../utils/stage';
import type { ServerEnvironment } from '../../modules/env';
import type { CollectionStats } from '../../modules/collection-stats';

const inputWidth = spacing[1400] * 3;
// width of options popover
Expand All @@ -38,11 +43,46 @@ type StageOperatorSelectProps = {
index: number;
selectedStage: string | null;
isDisabled: boolean;
stages: {
name: string;
env: ServerEnvironment[];
description: string;
}[];
stages: Stage[];
serverVersion: string;
isReadonlyView: boolean;
collectionStats: CollectionStats;
};

export type Stage = {
name: string;
env: ServerEnvironment[];
description: string;
};

export const getStageDescription = (
stage: Stage,
isReadonlyView: boolean,
versionIncompatibleCompass: boolean,
isPipelineSearchQueryable: boolean
) => {
if (isReadonlyView && isSearchStage(stage.name)) {
const minMajorMinorVersion =
VIEW_PIPELINE_UTILS.MIN_VERSION_FOR_VIEW_SEARCH_COMPATIBILITY_COMPASS.split(
'.'
)
.slice(0, 2)
.join('.');
if (versionIncompatibleCompass) {
return (
`Atlas only. Requires MongoDB ${minMajorMinorVersion}+ to run on a view. ` +
stage.description
);
}

if (!isPipelineSearchQueryable) {
return (
`Atlas only. Only views containing $addFields, $set or $match stages with the $expr operator are compatible with search indexes. ` +
stage.description
);
}
}
return (isAtlasOnly(stage.env) ? 'Atlas only. ' : '') + stage.description;
};

// exported for tests
Expand All @@ -51,14 +91,38 @@ export const StageOperatorSelect = ({
index,
selectedStage,
isDisabled,
serverVersion,
isReadonlyView,
collectionStats,
stages,
}: StageOperatorSelectProps) => {
const enableAtlasSearchIndexes = usePreference('enableAtlasSearchIndexes');
// filter out search stages for data explorer
const filteredStages =
isReadonlyView && !enableAtlasSearchIndexes
? stages.filter((stage) => !isSearchStage(stage.name))
: stages;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where is this requirement coming from? This is a weird change in compass behavior only for data explorer. Why are you not allowed to see actually working stages in aggregation builder based on a completely different feature?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removing this! Sorry for the back and forth. The final implementation will have both of them work the same for compass and de! Will ping you after I update this pr but both will have search stages with the same messages.


const onStageOperatorSelected = useCallback(
(name: string | null) => {
onChange(index, name);
},
[onChange, index]
);
const versionIncompatibleCompass =
!VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass(
serverVersion
);

const pipelineIsSearchQueryable = collectionStats?.pipeline
? VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(
collectionStats.pipeline as Document[]
)
: true;
const disableSearchStage =
isReadonlyView &&
(!pipelineIsSearchQueryable || versionIncompatibleCompass);

return (
<Combobox
value={selectedStage}
Expand All @@ -70,14 +134,18 @@ export const StageOperatorSelect = ({
data-testid="stage-operator-combobox"
className={comboboxStyles}
>
{stages.map((stage, index) => (
{filteredStages.map((stage: Stage, index) => (
<ComboboxOption
data-testid={`combobox-option-stage-${stage.name}`}
key={`combobox-option-stage-${index}`}
value={stage.name}
description={
(isAtlasOnly(stage.env) ? 'Atlas only. ' : '') + stage.description
}
disabled={isSearchStage(stage.name) && disableSearchStage}
description={getStageDescription(
stage,
isReadonlyView,
versionIncompatibleCompass,
pipelineIsSearchQueryable
)}
/>
))}
</Combobox>
Expand Down Expand Up @@ -113,6 +181,9 @@ export default withPreferences(
selectedStage: stage.stageOperator,
isDisabled: stage.disabled,
stages: stages,
serverVersion: state.serverVersion,
isReadonlyView: !!state.sourceName,
collectionStats: state.collectionStats,
};
},
(dispatch: any, ownProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import { isAction } from '../utils/is-action';

export type CollectionStats = {
document_count?: number;
pipeline?: unknown[];
};

export const INITIAL_STATE: CollectionStats = {
document_count: undefined,
pipeline: undefined,
};

export function pickCollectionStats(collection: Collection): CollectionStats {
const { document_count } = collection.toJSON();
const { document_count, pipeline } = collection.toJSON();
return {
document_count,
pipeline,
};
}

Expand Down
Loading