Skip to content

Commit 7d3b82a

Browse files
feat(compass-saved-aggregations-queries): update namespace for saved aggregations/queries (COMPASS-7665) (#5462)
1 parent f0655ab commit 7d3b82a

File tree

3 files changed

+194
-6
lines changed

3 files changed

+194
-6
lines changed

packages/compass-e2e-tests/tests/instance-my-queries-tab.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,117 @@ describe('Instance my queries tab', function () {
244244
const namespace = await browser.getActiveTabNamespace();
245245
expect(namespace).to.equal('test.numbers');
246246
});
247+
248+
context(
249+
'when a user has a saved query associated with a collection that does not exist',
250+
function () {
251+
const favoriteQueryName = 'list of numbers greater than 10 - query';
252+
const newCollectionName = 'numbers-renamed';
253+
254+
/** saves a query and renames the collection associated with the query, so that the query must be opened with the "select namespace" modal */
255+
async function setup() {
256+
// Run a query
257+
await browser.navigateToCollectionTab('test', 'numbers', 'Documents');
258+
await browser.runFindOperation('Documents', `{i: {$gt: 10}}`, {
259+
limit: '10',
260+
});
261+
await browser.clickVisible(Selectors.QueryBarHistoryButton);
262+
263+
// Wait for the popover to show
264+
const history = await browser.$(Selectors.QueryBarHistory);
265+
await history.waitForDisplayed();
266+
267+
// wait for the recent item to show.
268+
const recentCard = await browser.$(Selectors.QueryHistoryRecentItem);
269+
await recentCard.waitForDisplayed();
270+
271+
// Save the ran query
272+
await browser.hover(Selectors.QueryHistoryRecentItem);
273+
await browser.clickVisible(Selectors.QueryHistoryFavoriteAnItemButton);
274+
await browser.setValueVisible(
275+
Selectors.QueryHistoryFavoriteItemNameField,
276+
favoriteQueryName
277+
);
278+
await browser.clickVisible(
279+
Selectors.QueryHistorySaveFavoriteItemButton
280+
);
281+
282+
await browser.closeWorkspaceTabs();
283+
await browser.navigateToInstanceTab('Databases');
284+
await browser.navigateToInstanceTab('My Queries');
285+
286+
// open the menu
287+
await openMenuForQueryItem(browser, favoriteQueryName);
288+
289+
// copy to clipboard
290+
await browser.clickVisible(Selectors.SavedItemMenuItemCopy);
291+
292+
if (process.env.COMPASS_E2E_DISABLE_CLIPBOARD_USAGE !== 'true') {
293+
await browser.waitUntil(
294+
async () => {
295+
const text = (await clipboard.read())
296+
.replace(/\s+/g, ' ')
297+
.replace(/\n/g, '');
298+
const isValid =
299+
text ===
300+
'{ "collation": null, "filter": { "i": { "$gt": 10 } }, "limit": 10, "project": null, "skip": null, "sort": null }';
301+
if (!isValid) {
302+
console.log(text);
303+
}
304+
return isValid;
305+
},
306+
{ timeoutMsg: 'Expected copy to clipboard to work' }
307+
);
308+
}
309+
310+
// rename the collection associated with the query to force the open item modal
311+
await browser.shellEval('use test');
312+
await browser.shellEval(
313+
`db.numbers.renameCollection('${newCollectionName}')`
314+
);
315+
await browser.clickVisible(Selectors.SidebarRefreshDatabasesButton);
316+
}
317+
beforeEach(setup);
318+
319+
it('users can permanently associate a new namespace for an aggregation/query', async function () {
320+
await browser.navigateToInstanceTab('My Queries');
321+
// browse to the query
322+
await browser.clickVisible(Selectors.myQueriesItem(favoriteQueryName));
323+
324+
// the open item modal - select a new collection
325+
const openModal = await browser.$(Selectors.OpenSavedItemModal);
326+
await openModal.waitForDisplayed();
327+
await browser.selectOption(
328+
Selectors.OpenSavedItemDatabaseField,
329+
'test'
330+
);
331+
await browser.selectOption(
332+
Selectors.OpenSavedItemCollectionField,
333+
newCollectionName
334+
);
335+
336+
await browser.clickParent(
337+
'[data-testid="update-query-aggregation-checkbox"]'
338+
);
339+
340+
const confirmOpenButton = await browser.$(
341+
Selectors.OpenSavedItemModalConfirmButton
342+
);
343+
await confirmOpenButton.waitForEnabled();
344+
345+
await confirmOpenButton.click();
346+
await openModal.waitForDisplayed({ reverse: true });
347+
348+
await browser.navigateToInstanceTab('My Queries');
349+
350+
const [databaseNameElement, collectionNameElement] = [
351+
await browser.$('span=test'),
352+
await browser.$(`span=${newCollectionName}`),
353+
];
354+
355+
await databaseNameElement.waitForDisplayed();
356+
await collectionNameElement.waitForDisplayed();
357+
});
358+
}
359+
);
247360
});

packages/compass-saved-aggregations-queries/src/components/open-item-modal.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import {
3+
Checkbox,
34
FormModal,
45
Option,
56
Select,
@@ -14,6 +15,7 @@ import {
1415
openSelectedItem,
1516
selectCollection,
1617
selectDatabase,
18+
updateItemNamespaceChecked,
1719
} from '../stores/open-item';
1820

1921
type AsyncItemsSelectProps = {
@@ -98,15 +100,18 @@ type OpenItemModalProps = {
98100
itemName: string;
99101
isModalOpen: boolean;
100102
isSubmitDisabled: boolean;
103+
updateItemNamespace: boolean;
101104
onSubmit(): void;
102105
onClose(): void;
106+
onUpdateNamespaceChecked(checked: boolean): void;
103107
};
104108

105109
const modalContent = css({
106110
display: 'grid',
107111
gridTemplateAreas: `
108112
'description description'
109113
'database collection'
114+
'checkbox checkbox'
110115
`,
111116
gridAutoColumns: '1fr',
112117
rowGap: spacing[4],
@@ -125,20 +130,26 @@ const collectionSelect = css({
125130
gridArea: 'collection',
126131
});
127132

133+
const checkbox = css({
134+
gridArea: 'checkbox',
135+
});
136+
128137
const OpenItemModal: React.FunctionComponent<OpenItemModalProps> = ({
129138
namespace,
130139
itemType,
131140
itemName,
132141
isModalOpen,
133142
isSubmitDisabled,
143+
updateItemNamespace,
134144
onClose,
135145
onSubmit,
146+
onUpdateNamespaceChecked,
136147
}) => {
137148
return (
138149
<FormModal
139150
open={isModalOpen}
140151
onCancel={onClose}
141-
onSubmit={onSubmit}
152+
onSubmit={() => onSubmit()}
142153
title="Select a Namespace"
143154
submitButtonText="Open"
144155
submitDisabled={isSubmitDisabled}
@@ -161,6 +172,15 @@ const OpenItemModal: React.FunctionComponent<OpenItemModalProps> = ({
161172
label="Collection"
162173
></CollectionSelect>
163174
</div>
175+
<Checkbox
176+
className={checkbox}
177+
checked={updateItemNamespace}
178+
onChange={(event) => {
179+
onUpdateNamespaceChecked(event.target.checked);
180+
}}
181+
label={`Update this ${itemType} with the newly selected namespace`}
182+
data-testid="update-query-aggregation-checkbox"
183+
/>
164184
</div>
165185
</FormModal>
166186
);
@@ -169,7 +189,12 @@ const OpenItemModal: React.FunctionComponent<OpenItemModalProps> = ({
169189
const mapState: MapStateToProps<
170190
Pick<
171191
OpenItemModalProps,
172-
'isModalOpen' | 'isSubmitDisabled' | 'namespace' | 'itemType' | 'itemName'
192+
| 'isModalOpen'
193+
| 'isSubmitDisabled'
194+
| 'namespace'
195+
| 'itemType'
196+
| 'itemName'
197+
| 'updateItemNamespace'
173198
>,
174199
Record<string, never>,
175200
RootState
@@ -179,6 +204,7 @@ const mapState: MapStateToProps<
179204
selectedDatabase,
180205
selectedCollection,
181206
selectedItem: item,
207+
updateItemNamespace,
182208
},
183209
}) => {
184210
return {
@@ -187,15 +213,17 @@ const mapState: MapStateToProps<
187213
namespace: `${item?.database ?? ''}.${item?.collection ?? ''}`,
188214
itemName: item?.name ?? '',
189215
itemType: item?.type ?? '',
216+
updateItemNamespace,
190217
};
191218
};
192219

193220
const mapDispatch: MapDispatchToProps<
194-
Pick<OpenItemModalProps, 'onSubmit' | 'onClose'>,
221+
Pick<OpenItemModalProps, 'onSubmit' | 'onClose' | 'onUpdateNamespaceChecked'>,
195222
Record<string, never>
196223
> = {
197224
onSubmit: openSelectedItem,
198225
onClose: closeModal,
226+
onUpdateNamespaceChecked: updateItemNamespaceChecked,
199227
};
200228

201229
export default connect(mapState, mapDispatch)(OpenItemModal);

packages/compass-saved-aggregations-queries/src/stores/open-item.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type State = {
1414
collections: string[];
1515
selectedCollection: string | null;
1616
collectionsStatus: Status;
17+
updateItemNamespace: boolean;
1718
};
1819

1920
const INITIAL_STATE: State = {
@@ -26,6 +27,7 @@ const INITIAL_STATE: State = {
2627
collections: [],
2728
selectedCollection: null,
2829
collectionsStatus: 'initial',
30+
updateItemNamespace: false,
2931
};
3032

3133
export enum ActionTypes {
@@ -40,6 +42,7 @@ export enum ActionTypes {
4042
LoadCollections = 'compass-saved-aggregations-queries/loadCollections',
4143
LoadCollectionsSuccess = 'compass-saved-aggregations-queries/loadCollectionsSuccess',
4244
LoadCollectionsError = 'compass-saved-aggregations-queries/loadCollectionsError',
45+
UpdateNamespaceChecked = 'compass-saved-aggregations-queries/updateNamespaceChecked',
4346
}
4447

4548
type OpenModalAction = {
@@ -92,6 +95,11 @@ type LoadCollectionsErrorAction = {
9295
type: ActionTypes.LoadCollectionsError;
9396
};
9497

98+
type UpdateNamespaceChecked = {
99+
type: ActionTypes.UpdateNamespaceChecked;
100+
updateItemNamespace: boolean;
101+
};
102+
95103
export type Actions =
96104
| OpenModalAction
97105
| CloseModalAction
@@ -103,7 +111,8 @@ export type Actions =
103111
| SelectCollectionAction
104112
| LoadCollectionsAction
105113
| LoadCollectionsErrorAction
106-
| LoadCollectionsSuccessAction;
114+
| LoadCollectionsSuccessAction
115+
| UpdateNamespaceChecked;
107116

108117
const reducer: Reducer<State> = (state = INITIAL_STATE, action) => {
109118
switch (action.type) {
@@ -165,11 +174,21 @@ const reducer: Reducer<State> = (state = INITIAL_STATE, action) => {
165174
collections: action.collections,
166175
collectionsStatus: 'ready',
167176
};
177+
case ActionTypes.UpdateNamespaceChecked:
178+
return {
179+
...state,
180+
updateItemNamespace: action.updateItemNamespace,
181+
};
168182
default:
169183
return state;
170184
}
171185
};
172186

187+
export const updateItemNamespaceChecked = (updateItemNamespace: boolean) => ({
188+
type: ActionTypes.UpdateNamespaceChecked,
189+
updateItemNamespace,
190+
});
191+
173192
const openModal =
174193
(selectedItem: Item): SavedQueryAggregationThunkAction<Promise<void>> =>
175194
async (dispatch, _getState, { instance, dataService }) => {
@@ -249,16 +268,44 @@ export const openSavedItem =
249268
dispatch(openItem(item, database, collection));
250269
};
251270

271+
export const updateNamespaceChecked =
272+
(updateNamespaceChecked: boolean): SavedQueryAggregationThunkAction<void> =>
273+
(dispatch) => {
274+
dispatch({
275+
type: ActionTypes.UpdateNamespaceChecked,
276+
updateNamespaceChecked,
277+
});
278+
};
279+
252280
export const openSelectedItem =
253-
(): SavedQueryAggregationThunkAction<void> => (dispatch, getState) => {
281+
(): SavedQueryAggregationThunkAction<Promise<void>> =>
282+
async (dispatch, getState, { queryStorage, pipelineStorage }) => {
254283
const {
255-
openItem: { selectedItem, selectedDatabase, selectedCollection },
284+
openItem: {
285+
selectedItem,
286+
selectedDatabase,
287+
selectedCollection,
288+
updateItemNamespace,
289+
},
256290
} = getState();
257291

258292
if (!selectedItem || !selectedDatabase || !selectedCollection) {
259293
return;
260294
}
261295

296+
if (updateItemNamespace) {
297+
const id = selectedItem.id;
298+
const newNamespace = `${selectedDatabase}.${selectedCollection}`;
299+
300+
if (selectedItem.type === 'aggregation') {
301+
await pipelineStorage?.updateAttributes(id, {
302+
namespace: newNamespace,
303+
});
304+
} else if (selectedItem.type === 'query') {
305+
await queryStorage?.updateAttributes(id, { _ns: newNamespace });
306+
}
307+
}
308+
262309
dispatch({ type: ActionTypes.CloseModal });
263310
dispatch(openItem(selectedItem, selectedDatabase, selectedCollection));
264311
};

0 commit comments

Comments
 (0)