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
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,44 @@ import { render, screen, userEvent } from '@mongodb-js/testing-library-compass';
import { expect } from 'chai';
import sinon from 'sinon';
import { ValidationEditor } from './validation-editor';
import { PreferencesProvider } from 'compass-preferences-model/provider';
import { createSandboxFromDefaultPreferences } from 'compass-preferences-model';

function renderValidationEditor(
async function renderValidationEditor(
props: Partial<React.ComponentProps<typeof ValidationEditor>>
) {
const validation = {
validator: '',
validator: '{}',
validationAction: 'warn',
validationLevel: 'moderate',
isChanged: false,
syntaxError: null,
error: null,
} as const;

const preferences = await createSandboxFromDefaultPreferences();
await preferences.savePreferences({ enableExportSchema: true });

return render(
<ValidationEditor
namespace="test.test"
validatorChanged={() => {}}
validationActionChanged={() => {}}
validationLevelChanged={() => {}}
cancelValidation={() => {}}
saveValidation={() => {}}
clearSampleDocuments={() => {}}
serverVersion="8.0.5"
onClickEnableEditRules={() => {}}
validation={validation}
isEditable
isEditingEnabled
{...props}
/>
<PreferencesProvider value={preferences}>
<ValidationEditor
namespace="test.test"
validatorChanged={() => {}}
validationActionChanged={() => {}}
validationLevelChanged={() => {}}
cancelValidation={() => {}}
saveValidation={() => {}}
clearSampleDocuments={() => {}}
serverVersion="8.0.5"
onClickEnableEditRules={() => {}}
validation={validation}
generateValidationRules={() => {}}
isRulesGenerationInProgress={false}
isEditable
isEditingEnabled
{...props}
/>
</PreferencesProvider>
);
}

Expand All @@ -40,9 +49,9 @@ describe('ValidationEditor [Component]', function () {
'when it is an editable mode but editing is not yet enabled',
function () {
let onClickEnableEditRulesSpy: sinon.SinonSpy;
beforeEach(function () {
beforeEach(async function () {
onClickEnableEditRulesSpy = sinon.spy();
renderValidationEditor({
await renderValidationEditor({
onClickEnableEditRules: onClickEnableEditRulesSpy,
isEditingEnabled: false,
isEditable: true,
Expand All @@ -62,8 +71,8 @@ describe('ValidationEditor [Component]', function () {
);

context('when it is an editable mode and editing is enabled', function () {
beforeEach(function () {
renderValidationEditor({
beforeEach(async function () {
await renderValidationEditor({
isEditable: true,
isEditingEnabled: true,
});
Expand All @@ -78,8 +87,8 @@ describe('ValidationEditor [Component]', function () {
});

context('when it is a not editable mode', function () {
beforeEach(function () {
renderValidationEditor({
beforeEach(async function () {
await renderValidationEditor({
isEditable: false,
});
});
Expand All @@ -91,4 +100,83 @@ describe('ValidationEditor [Component]', function () {
).to.not.exist;
});
});

context('when the validator is empty', function () {
let onGenerateRulesSpy: sinon.SinonSpy;
beforeEach(async function () {
onGenerateRulesSpy = sinon.spy();
await renderValidationEditor({
generateValidationRules: onGenerateRulesSpy,
isEditable: true,
validation: {
validator: '',
validationAction: 'error',
validationLevel: 'moderate',
isChanged: false,
syntaxError: null,
error: null,
},
});
});

it('allows to generate rules', function () {
const generateBtn = screen.getByRole('button', {
name: 'Generate rules',
});
expect(generateBtn).to.be.visible;
userEvent.click(generateBtn);
expect(onGenerateRulesSpy).to.have.been.calledOnce;
});
});

context('when the validator is empty object', function () {
let onGenerateRulesSpy: sinon.SinonSpy;
beforeEach(async function () {
onGenerateRulesSpy = sinon.spy();
await renderValidationEditor({
generateValidationRules: onGenerateRulesSpy,
isEditable: true,
validation: {
validator: '{}',
validationAction: 'error',
validationLevel: 'moderate',
isChanged: false,
syntaxError: null,
error: null,
},
});
});

it('allows to generate rules', function () {
const generateBtn = screen.getByRole('button', {
name: 'Generate rules',
});
expect(generateBtn).to.be.visible;
userEvent.click(generateBtn);
expect(onGenerateRulesSpy).to.have.been.calledOnce;
});
});

context('when rules generation is in progress', function () {
beforeEach(async function () {
await renderValidationEditor({
isEditable: true,
isRulesGenerationInProgress: true,
validation: {
validator: '{}',
validationAction: 'error',
validationLevel: 'moderate',
isChanged: false,
syntaxError: null,
error: null,
},
});
});

it('allows to generate rules', function () {
const generateBtn = screen.getByTestId('generate-rules-button');
expect(generateBtn).to.be.visible;
expect(generateBtn).to.have.attribute('aria-disabled', 'true');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
useConfirmationModal,
useDarkMode,
KeylineCard,
ButtonVariant,
SpinLoader,
Tooltip,
} from '@mongodb-js/compass-components';
import {
CodemirrorMultilineEditor,
Expand All @@ -39,6 +42,8 @@ import {
} from '../modules/validation';
import { clearSampleDocuments } from '../modules/sample-documents';
import { enableEditRules } from '../modules/edit-mode';
import { usePreference } from 'compass-preferences-model/provider';
import { generateValidationRules } from '../modules/rules-generation';

const validationEditorStyles = css({
padding: spacing[400],
Expand All @@ -48,6 +53,10 @@ const validationOptionsStyles = css({
display: 'flex',
});

const generateButtonContainerStyles = css({
flexGrow: 1,
});

const actionsStyles = css({
display: 'flex',
alignItems: 'center',
Expand Down Expand Up @@ -126,6 +135,7 @@ type ValidationEditorProps = {
validationLevelChanged: (level: ValidationLevel) => void;
cancelValidation: () => void;
saveValidation: (text: Validation) => void;
generateValidationRules: () => void;
serverVersion: string;
validation: Pick<
ValidationState,
Expand All @@ -138,6 +148,7 @@ type ValidationEditorProps = {
>;
isEditable: boolean;
isEditingEnabled: boolean;
isRulesGenerationInProgress: boolean;
};

/**
Expand All @@ -154,11 +165,14 @@ export const ValidationEditor: React.FunctionComponent<
validationLevelChanged,
cancelValidation,
saveValidation,
generateValidationRules,
serverVersion,
validation,
isEditable,
isEditingEnabled,
isRulesGenerationInProgress,
}) => {
const enableExportSchema = usePreference('enableExportSchema');
const track = useTelemetry();
const connectionInfoRef = useConnectionInfoRef();
const { showConfirmation } = useConfirmationModal();
Expand Down Expand Up @@ -227,12 +241,43 @@ export const ValidationEditor: React.FunctionComponent<
saveValidationRef.current(validationRef.current);
}, [showConfirmation]);

const isEmpty = useMemo<boolean>(() => {
if (!validation.validator || validation.validator.length === 0) return true;
try {
return Object.keys(JSON.parse(validation.validator)).length === 0;
} catch {
return false;
}
}, [validation.validator]);

return (
<KeylineCard
data-testid="validation-editor"
className={validationEditorStyles}
>
<div className={validationOptionsStyles}>
{enableExportSchema && (
<div className={generateButtonContainerStyles}>
<Tooltip
enabled={!isEmpty}
trigger={
<Button
data-testid="generate-rules-button"
disabled={!isEmpty}
isLoading={isRulesGenerationInProgress}
loadingIndicator={<SpinLoader />}
onClick={generateValidationRules}
variant={ButtonVariant.Primary}
size="small"
>
Generate rules
</Button>
}
>
Clear existing rules before generating new ones
</Tooltip>
</div>
)}
<ActionSelector
isEditable={isEditable && isEditingEnabled}
validationActionChanged={validationActionChanged}
Expand Down Expand Up @@ -326,6 +371,7 @@ const mapStateToProps = (state: RootState) => ({
isEditingEnabled: state.editMode.isEditingEnabledByUser,
validation: state.validation,
namespace: state.namespace.ns,
isRulesGenerationInProgress: state.rulesGeneration.isInProgress,
});

/**
Expand All @@ -339,4 +385,5 @@ export default connect(mapStateToProps, {
onClickEnableEditRules: enableEditRules,
validationActionChanged,
validationLevelChanged,
generateValidationRules,
})(ValidationEditor);
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const LEVEL_HELP_URL =

const validationOptionStyles = css({
display: 'flex',
marginRight: spacing[4],
marginLeft: spacing[4],
alignItems: 'center',
});

Expand All @@ -47,7 +47,7 @@ export function ActionSelector({

return (
<div className={validationOptionStyles}>
<Label htmlFor={controlId}>Validation Action</Label>
<Label htmlFor={controlId}>Action</Label>
<IconButton
href={ACTION_HELP_URL}
target="_blank"
Expand Down Expand Up @@ -88,7 +88,7 @@ export function LevelSelector({

return (
<div className={validationOptionStyles}>
<Label htmlFor={controlId}>Validation Level</Label>
<Label htmlFor={controlId}>Level</Label>
<IconButton
href={LEVEL_HELP_URL}
target="_blank"
Expand Down
Loading