Skip to content

Commit 1f064ba

Browse files
authored
feat(stage-wizard): dynamic stage wizard fields (#4268)
1 parent d655e70 commit 1f064ba

File tree

7 files changed

+208
-15
lines changed

7 files changed

+208
-15
lines changed

packages/compass-aggregations/src/components/aggregation-side-panel/stage-wizard-use-cases/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type StageWizardUseCase = {
66
title: string;
77
stageOperator: string;
88
wizardComponent: React.FunctionComponent<{
9+
fields: string[];
910
onChange: (value: string, validationError: Error | null) => void;
1011
}>;
1112
serverVersion?: string;
@@ -15,7 +16,7 @@ export const STAGE_WIZARD_USE_CASES: StageWizardUseCase[] = [
1516
{
1617
id: 'sort',
1718
title:
18-
'Sort documents in [ascending/descending] order based on a single or a set of fields.',
19+
'Sort documents in [ascending/descending] order based on a single or a set of fields',
1920
stageOperator: '$sort',
2021
wizardComponent: SortUseCase,
2122
},

packages/compass-aggregations/src/components/aggregation-side-panel/stage-wizard-use-cases/sort/sort.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {
99
ComboboxWithCustomOption,
1010
} from '@mongodb-js/compass-components';
1111
import React, { useEffect, useMemo, useState } from 'react';
12-
import { connect } from 'react-redux';
13-
import type { RootState } from '../../../../modules';
1412

1513
const SORT_DIRECTION_OPTIONS = [
1614
{
@@ -184,6 +182,4 @@ export const SortForm = ({
184182
);
185183
};
186184

187-
export default connect((state: RootState) => ({
188-
fields: state.fields.map((x: { name: string }) => x.name),
189-
}))(SortForm);
185+
export default SortForm;

packages/compass-aggregations/src/components/aggregation-side-panel/stage-wizard-use-cases/use-case-list.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React from 'react';
22
import {
3-
Badge,
43
Body,
54
css,
65
KeylineCard,
6+
Link,
77
spacing,
88
} from '@mongodb-js/compass-components';
99
import { STAGE_WIZARD_USE_CASES } from '.';
10+
import { getStageHelpLink } from '../../../utils/stage';
1011

1112
const cardStyles = css({
1213
cursor: 'pointer',
@@ -30,7 +31,12 @@ const UseCaseList = ({ onSelect }: { onSelect: (id: string) => void }) => {
3031
className={cardStyles}
3132
>
3233
<Body className={cardTitleStyles}>{title}</Body>
33-
<Badge>{stageOperator}</Badge>
34+
<Link
35+
target="_blank"
36+
href={getStageHelpLink(stageOperator) as string}
37+
>
38+
{stageOperator}
39+
</Link>
3440
</KeylineCard>
3541
);
3642
})}

packages/compass-aggregations/src/components/stage-wizard/index.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const renderStageWizard = (
2121
setNodeRef={() => {}}
2222
style={{}}
2323
listeners={undefined}
24+
fields={[]}
2425
{...props}
2526
/>
2627
);

packages/compass-aggregations/src/components/stage-wizard/index.tsx

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { useCallback, useMemo, useState } from 'react';
22
import {
3-
Badge,
43
Body,
54
Button,
65
css,
76
KeylineCard,
7+
Link,
88
spacing,
99
WarningSummary,
1010
} from '@mongodb-js/compass-components';
@@ -17,7 +17,12 @@ import {
1717
removeWizard,
1818
updateWizardValue,
1919
} from '../../modules/pipeline-builder/stage-editor';
20-
import type { Wizard } from '../../modules/pipeline-builder/stage-editor';
20+
import type {
21+
StoreStage,
22+
Wizard,
23+
} from '../../modules/pipeline-builder/stage-editor';
24+
import { getSchema } from '../../utils/get-schema';
25+
import { getStageHelpLink } from '../../utils/stage';
2126
import type { SortableProps } from '../pipeline-builder-workspace/pipeline-builder-ui-workspace/sortable-list';
2227

2328
const containerStyles = css({
@@ -58,6 +63,7 @@ type StageWizardProps = SortableProps & {
5863
useCaseId: string;
5964
value: string | null;
6065
syntaxError: SyntaxError | null;
66+
fields: string[];
6167
onChange: (value: string) => void;
6268
onCancel: () => void;
6369
onApply: () => void;
@@ -68,6 +74,7 @@ export const StageWizard = ({
6874
useCaseId,
6975
value,
7076
syntaxError,
77+
fields,
7178
onChange,
7279
onCancel,
7380
onApply,
@@ -108,12 +115,20 @@ export const StageWizard = ({
108115
<div {...listeners}>
109116
<div className={headerStyles}>
110117
<Body weight="medium">{useCase.title}</Body>
111-
<Badge>{useCase.stageOperator}</Badge>
118+
<Link
119+
target="_blank"
120+
href={getStageHelpLink(useCase.stageOperator) as string}
121+
>
122+
{useCase.stageOperator}
123+
</Link>
112124
</div>
113125
</div>
114126
<div className={wizardContentStyles}>
115127
<div data-testid="wizard-form">
116-
<useCase.wizardComponent onChange={onChangeWizard} />
128+
<useCase.wizardComponent
129+
fields={fields}
130+
onChange={onChangeWizard}
131+
/>
117132
</div>
118133
<div className={cardFooterStyles}>
119134
<div>
@@ -145,15 +160,37 @@ type WizardOwnProps = {
145160

146161
export default connect(
147162
(state: RootState, ownProps: WizardOwnProps) => {
148-
const wizard = state.pipelineBuilder.stageEditor.stages[
149-
ownProps.index
150-
] as Wizard;
163+
const {
164+
autoPreview,
165+
fields: initialFields,
166+
pipelineBuilder: {
167+
stageEditor: { stages },
168+
},
169+
} = state;
170+
171+
const wizard = stages[ownProps.index] as Wizard;
172+
173+
const previousStage = stages
174+
.slice(0, ownProps.index)
175+
.reverse()
176+
.find((x): x is StoreStage => x.type === 'stage' && !x.disabled);
177+
178+
const mappedInitialFields = initialFields.map(
179+
(x: { name: string }) => x.name
180+
);
181+
const previousStageFields = getSchema(previousStage?.previewDocs ?? []);
182+
183+
const fields =
184+
previousStageFields.length > 0 && autoPreview
185+
? previousStageFields
186+
: mappedInitialFields;
151187

152188
return {
153189
id: wizard.id,
154190
syntaxError: wizard.syntaxError,
155191
useCaseId: wizard.useCaseId,
156192
value: wizard.value,
193+
fields,
157194
};
158195
},
159196
(dispatch: PipelineBuilderThunkDispatch, ownProps: WizardOwnProps) => ({
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { expect } from 'chai';
2+
import bson from 'bson';
3+
import { getSchema } from './get-schema';
4+
5+
const DATA = [
6+
{
7+
useCase: '_id is always the first one',
8+
input: [
9+
{
10+
data: 'something',
11+
_id: 123456,
12+
},
13+
],
14+
output: ['_id', 'data'],
15+
},
16+
{
17+
useCase: 'simple json object',
18+
input: [
19+
{
20+
name: 'hi',
21+
data: 'hello',
22+
},
23+
],
24+
output: ['data', 'name'],
25+
},
26+
{
27+
useCase: 'nested json object',
28+
input: [
29+
{
30+
name: 'hi',
31+
data: 'hello',
32+
address: {
33+
city: 'berlin',
34+
street: {
35+
name: 'Alt-Moabit',
36+
number: 1,
37+
},
38+
},
39+
},
40+
],
41+
output: [
42+
'address',
43+
'address.city',
44+
'address.street',
45+
'address.street.name',
46+
'address.street.number',
47+
'data',
48+
'name',
49+
],
50+
},
51+
{
52+
useCase: 'multiple nested json object with array values',
53+
input: [
54+
{
55+
data: 'hello',
56+
name: 'hi',
57+
streets: [
58+
{
59+
name: 'Alt-Moabit',
60+
zip: 10555,
61+
},
62+
{
63+
name: 'Alt-Moabit',
64+
number: 12,
65+
},
66+
],
67+
},
68+
{
69+
data: 'hello',
70+
name: 'hi',
71+
streets: [
72+
{
73+
_id: 1234,
74+
city: 'Berlin',
75+
},
76+
],
77+
},
78+
],
79+
output: [
80+
'data',
81+
'name',
82+
'streets',
83+
'streets._id',
84+
'streets.city',
85+
'streets.name',
86+
'streets.number',
87+
'streets.zip',
88+
],
89+
},
90+
{
91+
useCase: 'handles bson values',
92+
input: [
93+
{
94+
_id: new bson.ObjectId(),
95+
data: new bson.Int32(123),
96+
address: {
97+
street: 'Alt-Moabit',
98+
number: new bson.Int32(18),
99+
},
100+
},
101+
],
102+
output: ['_id', 'address', 'address.number', 'address.street', 'data'],
103+
},
104+
];
105+
106+
describe('get schema', function () {
107+
DATA.forEach(({ useCase, input, output }) => {
108+
it(useCase, function () {
109+
expect(getSchema(input)).to.deep.equal(output);
110+
});
111+
});
112+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Document } from 'mongodb';
2+
3+
const getArrayKeys = (records: Document[]) => {
4+
return records
5+
.map((item) => getObjectKeys(item))
6+
.flat()
7+
.filter((x, i, a) => a.indexOf(x) === i)
8+
.sort();
9+
};
10+
11+
const getObjectKeys = (record: Document) => {
12+
const keys: string[] = [];
13+
14+
if (!record) {
15+
return keys;
16+
}
17+
18+
for (const key in record) {
19+
keys.push(key);
20+
const value = record[key];
21+
22+
if (value && typeof value === 'object') {
23+
const isBson = value._bsontype;
24+
if (!isBson) {
25+
const nestedKeys = Array.isArray(value)
26+
? getArrayKeys(value)
27+
: getObjectKeys(value);
28+
nestedKeys.forEach((nestedKey) => {
29+
keys.push(`${key}.${nestedKey}`);
30+
});
31+
}
32+
}
33+
}
34+
35+
return keys;
36+
};
37+
38+
export const getSchema = (data: Document[]) => {
39+
return getArrayKeys(data);
40+
};

0 commit comments

Comments
 (0)