Skip to content
Draft
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
68b9e53
init
syn-zhu Aug 29, 2025
a9ea81e
ef
syn-zhu Aug 29, 2025
fc3f50c
ef
syn-zhu Sep 9, 2025
5932624
ef
syn-zhu Sep 9, 2025
8cd1ac5
ef
syn-zhu Sep 9, 2025
b5c6f66
ef
syn-zhu Sep 10, 2025
b308225
restore connections
syn-zhu Sep 16, 2025
a5713b4
refactor
syn-zhu Sep 17, 2025
2351763
refactor
syn-zhu Sep 17, 2025
619362d
break something
syn-zhu Sep 17, 2025
25cf5e6
break something
syn-zhu Sep 17, 2025
e3876f9
fix imports
syn-zhu Sep 17, 2025
8908793
fix imports
syn-zhu Sep 17, 2025
920f572
refactor
syn-zhu Sep 17, 2025
aa78f68
refactor
syn-zhu Sep 17, 2025
42a15e0
add workspaces state service
syn-zhu Sep 19, 2025
9736436
add dekstop storage
syn-zhu Sep 19, 2025
3fd3b6b
implement web storage provider
syn-zhu Sep 19, 2025
04b30c3
make the services work
syn-zhu Sep 30, 2025
321a649
move effect callbacks to service
syn-zhu Sep 30, 2025
b667dd5
remove comment
syn-zhu Sep 30, 2025
27bae4b
provider remove
syn-zhu Sep 30, 2025
0d14953
remove unused stuff
syn-zhu Sep 30, 2025
2932698
cover decline restore
syn-zhu Oct 1, 2025
88c8435
fix things
syn-zhu Oct 4, 2025
da2be3e
add providers
syn-zhu Oct 4, 2025
b2eb338
fix bug
syn-zhu Oct 4, 2025
9f9bab3
add new files
syn-zhu Oct 4, 2025
b0af4d9
filter out welcome tabs
syn-zhu Oct 4, 2025
992f191
merge
syn-zhu Oct 4, 2025
13e2c7d
refactor storage
syn-zhu Oct 4, 2025
097a002
fix logging
syn-zhu Oct 4, 2025
4f1d184
remove space
syn-zhu Oct 5, 2025
5aeb75d
undo typeof
syn-zhu Oct 5, 2025
ef2d42e
remove accidental changes
syn-zhu Oct 5, 2025
a7df6a5
debounce
syn-zhu Oct 8, 2025
76ae816
typing
syn-zhu Oct 8, 2025
a6d7db9
comments
syn-zhu Oct 11, 2025
139f4f4
merge
syn-zhu Oct 11, 2025
e4f9a7a
feature flag
syn-zhu Oct 11, 2025
dec0e41
pref
syn-zhu Oct 11, 2025
cac3263
Merge branch 'main' into smnzhu/save-tab
syn-zhu Oct 14, 2025
a8c7218
address some comments
syn-zhu Oct 24, 2025
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
6 changes: 5 additions & 1 deletion packages/atlas-service/src/atlas-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export class AtlasService {
userDataEndpoint(
orgId: string,
groupId: string,
type: 'favoriteQueries' | 'recentQueries' | 'favoriteAggregations',
type:
| 'favoriteQueries'
| 'recentQueries'
| 'favoriteAggregations'
| 'savedWorkspaces',
id?: string
): string {
const encodedOrgId = encodeURIComponent(orgId);
Expand Down
8 changes: 8 additions & 0 deletions packages/compass-preferences-model/src/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type FeatureFlags = {
enableAIAssistant: boolean;
enablePerformanceInsightsEntrypoints: boolean;
enableAutomaticRelationshipInference: boolean;
enableRestoreWorkspaces: boolean;
};

export const featureFlags: Required<{
Expand Down Expand Up @@ -198,4 +199,11 @@ export const featureFlags: Required<{
'Enable automatic relationship inference during data model generation',
},
},

enableRestoreWorkspaces: {
stage: 'development',
description: {
short: 'Enable restoring previous workspace tabs on startup',
},
},
};
2 changes: 1 addition & 1 deletion packages/compass-user-data/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export type { ReadAllResult } from './user-data';
export { type IUserData, FileUserData, AtlasUserData } from './user-data';
export { IUserData, FileUserData, AtlasUserData } from './user-data';
export { z } from 'zod';
13 changes: 11 additions & 2 deletions packages/compass-web/src/entrypoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
} from '@mongodb-js/compass-workspaces';
import WorkspacesPlugin, {
WorkspacesProvider,
WorkspacesStorageServiceProviderWeb,
} from '@mongodb-js/compass-workspaces';
import {
CollectionsWorkspaceTab,
Expand Down Expand Up @@ -122,7 +123,8 @@ const WithStorageProviders = createServiceProvider(
const type = pathParts[0] as
| 'favoriteQueries'
| 'recentQueries'
| 'favoriteAggregations';
| 'favoriteAggregations'
| 'savedWorkspaces';
const pathOrgId = pathParts[1];
const pathProjectId = pathParts[2];
const id = pathParts[3];
Expand Down Expand Up @@ -173,7 +175,14 @@ const WithStorageProviders = createServiceProvider(
<PipelineStorageProvider value={pipelineStorage.current}>
<FavoriteQueryStorageProvider value={favoriteQueryStorage.current}>
<RecentQueryStorageProvider value={recentQueryStorage.current}>
{children}
<WorkspacesStorageServiceProviderWeb
orgId={orgId}
projectId={projectId}
getResourceUrl={getResourceUrl}
authenticatedFetch={authenticatedFetch}
>
{children}
</WorkspacesStorageServiceProviderWeb>
</RecentQueryStorageProvider>
</FavoriteQueryStorageProvider>
</PipelineStorageProvider>
Expand Down
22 changes: 21 additions & 1 deletion packages/compass-workspaces/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import workspacesReducer, {
connectionDisconnected,
updateDatabaseInfo,
updateCollectionInfo,
loadSavedWorkspaces,
beforeUnloading,
} from './stores/workspaces';
import Workspaces from './components';
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import { workspacesStateChangeMiddleware } from './stores/workspaces-middleware';
import type { MongoDBInstance } from '@mongodb-js/compass-app-stores/provider';
import { mongoDBInstancesManagerLocator } from '@mongodb-js/compass-app-stores/provider';
import type Collection from 'mongodb-collection-model';
Expand All @@ -39,13 +41,19 @@ import {
} from '@mongodb-js/compass-app-stores/provider';
import type { PreferencesAccess } from 'compass-preferences-model/provider';
import { preferencesLocator } from 'compass-preferences-model/provider';
import {
type WorkspacesStateSchema,
workspacesStorageServiceLocator,
} from './services/workspaces-storage';
import { type IUserData } from '../../compass-user-data/dist/user-data';

export type WorkspacesServices = {
globalAppRegistry: AppRegistry;
instancesManager: MongoDBInstancesManager;
connections: ConnectionsService;
logger: Logger;
preferences: PreferencesAccess;
userData: IUserData<typeof WorkspacesStateSchema>;
};

export function configureStore(
Expand All @@ -67,7 +75,10 @@ export function configureStore(
collectionInfo: {},
databaseInfo: {},
},
applyMiddleware(thunk.withExtraArgument(services))
applyMiddleware(
thunk.withExtraArgument(services),
workspacesStateChangeMiddleware(services)
)
);

return store;
Expand All @@ -87,6 +98,7 @@ export function activateWorkspacePlugin(
connections,
logger,
preferences,
userData,
}: WorkspacesServices,
{ on, cleanup, addCleanup }: ActivateHelpers
) {
Expand All @@ -96,8 +108,11 @@ export function activateWorkspacePlugin(
connections,
logger,
preferences,
userData,
});

void store.dispatch(loadSavedWorkspaces());

addCleanup(cleanupLocalAppRegistries);

const setupInstanceListeners = (
Expand Down Expand Up @@ -230,9 +245,11 @@ const WorkspacesPlugin = registerCompassPlugin(
connections: connectionsLocator,
logger: createLoggerLocator('COMPASS-WORKSPACES-UI'),
preferences: preferencesLocator,
userData: workspacesStorageServiceLocator,
}
);

export { WorkspacesStateSchema } from './services/workspaces-storage';
export default WorkspacesPlugin;
export { WorkspacesProvider } from './components/workspaces-provider';
export type { OpenWorkspaceOptions, CollectionTabInfo };
Expand All @@ -252,3 +269,6 @@ export type {
CollectionSubtab,
WorkspacePluginProps,
} from './types';

export { WorkspacesStorageServiceProviderDesktop } from './services/workspaces-storage-desktop';
export { WorkspacesStorageServiceProviderWeb } from './services/workspaces-storage-web';
2 changes: 2 additions & 0 deletions packages/compass-workspaces/src/services/index.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see this file ever being used, why did you added it?

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { WorkspacesStorageServiceProviderDesktop } from './workspaces-storage-desktop';
export { WorkspacesStorageServiceProviderWeb } from './workspaces-storage-web';
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useRef } from 'react';
import { FileUserData, type IUserData } from '@mongodb-js/compass-user-data';
import {
WorkspacesStateSchema,
WorkspacesStorageServiceContext,
} from './workspaces-storage';
import { EJSON } from 'bson';

export const WorkspacesStorageServiceProviderDesktop: React.FunctionComponent =
({ children }) => {
const storageRef = useRef<IUserData<typeof WorkspacesStateSchema>>(
new FileUserData(WorkspacesStateSchema, 'WorkspacesState', {
serialize: (content) =>
EJSON.stringify(content, {
relaxed: false,
}),
deserialize: (content: string) => EJSON.parse(content),
}) as IUserData<typeof WorkspacesStateSchema>
);
return (
<WorkspacesStorageServiceContext.Provider value={storageRef.current}>
{children}
</WorkspacesStorageServiceContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useRef } from 'react';
import { AtlasUserData, type IUserData } from '@mongodb-js/compass-user-data';
import {
WorkspacesStateSchema,
WorkspacesStorageServiceContext,
} from './workspaces-storage';
import { EJSON } from 'bson';

export const WorkspacesStorageServiceProviderWeb: React.FunctionComponent<{
orgId: string;
projectId: string;
getResourceUrl: (path?: string) => string;
authenticatedFetch: (
url: RequestInfo | URL,
options?: RequestInit
) => Promise<Response>;
}> = ({ orgId, projectId, getResourceUrl, authenticatedFetch, children }) => {
const storageRef = useRef<IUserData<typeof WorkspacesStateSchema>>(
new AtlasUserData(WorkspacesStateSchema, 'WorkspacesState', {
orgId,
projectId,
getResourceUrl,
authenticatedFetch,
serialize: (content) =>
EJSON.stringify(content, {
relaxed: false,
}),
deserialize: (content: string) => EJSON.parse(content),
})
);
return (
<WorkspacesStorageServiceContext.Provider value={storageRef.current}>
{children}
</WorkspacesStorageServiceContext.Provider>
);
};
112 changes: 112 additions & 0 deletions packages/compass-workspaces/src/services/workspaces-storage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { createServiceLocator } from '@mongodb-js/compass-app-registry';
import {
IUserData,
type ReadAllResult,
z,
} from '@mongodb-js/compass-user-data';
import React, { useContext } from 'react';
import { collectionSubtabValues } from '../types';

const CollectionSubtabSchema = z.enum(collectionSubtabValues);

export const WorkspaceTabSchema = z
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is better, but only half way there IMO. As I said before, these types are now even more of a copy of what's defined in types.ts. Can you maybe walk me through your thought process here? Is there any reason you don't want to replace existing types in types.ts with types derived from the schemas? I don't see the reason not to do it, but maybe there's one I'm missing 🙂 Like imagine next time someone needs to add a new workspace, now they need to add exactly the same types twice in two different places, is there a good reason to do so?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I just don't understand what the suggestion is 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

replace existing types in types.ts with types derived from the schemas

How exactly do I actually do this? I think I'm getting a bit mixed up on the terminology here, can we maybe refer to specific file / class names?

Copy link
Collaborator Author

@syn-zhu syn-zhu Oct 24, 2025

Choose a reason for hiding this comment

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

I would like to follow the suggestion but I don't actually understand what the suggestion is

It sounds like you're implying that there's some way to convert a typescript type (from types.ts) "directly" into a zod schema but I'm just not aware of how you would do that

.discriminatedUnion('type', [
z.object({
type: z.literal('Welcome'),
}),
z.object({
type: z.literal('My Queries'),
}),
z.object({
type: z.literal('Data Modeling'),
}),
z.object({
type: z.literal('Databases'),
connectionId: z.string(),
}),
z.object({
type: z.literal('Performance'),
connectionId: z.string(),
}),
z.object({
type: z.literal('Shell'),
connectionId: z.string(),
initialEvaluate: z.union([z.string(), z.array(z.string())]).optional(),
initialInput: z.string().optional(),
}),
z.object({
type: z.literal('Collections'),
connectionId: z.string(),
namespace: z.string(),
inferredFromPrivileges: z.boolean().optional(),
}),
z.object({
type: z.literal('Collection'),
subTab: CollectionSubtabSchema,
initialQuery: z.record(z.any()).optional(),
initialPipeline: z.array(z.record(z.any())).optional(),
initialPipelineText: z.string().optional(),
initialAggregation: z.record(z.any()).optional(),
editViewName: z.string().optional(),
connectionId: z.string(),
namespace: z.string(),
inferredFromPrivileges: z.boolean().optional(),
}),
])
.and(
z.object({
id: z.string(),
})
);

export const WorkspacesStateSchema = z.object({
tabs: z.array(WorkspaceTabSchema),
activeTabId: z.string().nullable(),
timestamp: z.number(),
});

// TypeScript types derived from the schemas
export type WorkspaceTabData = z.output<typeof WorkspaceTabSchema>;
export type WorkspacesStateData = z.output<typeof WorkspacesStateSchema>;

const throwIfNotTestEnv = () => {
if (process.env.NODE_ENV !== 'test') {
throw new Error("Can't find Workspaces storage service in React context");
}
};

export class noopUserData<T extends z.Schema> extends IUserData<T> {
write(): Promise<boolean> {
throwIfNotTestEnv();
return Promise.resolve(true);
}
delete(): Promise<boolean> {
throwIfNotTestEnv();
return Promise.resolve(true);
}
readAll(): Promise<ReadAllResult<T>> {
throwIfNotTestEnv();
return Promise.resolve({ data: [], errors: [] });
}
readOne(): Promise<z.output<T>> {
throwIfNotTestEnv();
return Promise.resolve(undefined);
}
updateAttributes(): Promise<boolean> {
throwIfNotTestEnv();
return Promise.resolve(true);
}
}

export const noopWorkspacesStorageService: IUserData<
typeof WorkspacesStateSchema
> = new noopUserData(WorkspacesStateSchema, 'WorkspacesState');

export const WorkspacesStorageServiceContext = React.createContext<
IUserData<typeof WorkspacesStateSchema>
>(noopWorkspacesStorageService);

export const workspacesStorageServiceLocator = createServiceLocator(() => {
const service = useContext(WorkspacesStorageServiceContext);
return service;
}, 'workspacesStorageServiceLocator');
Loading
Loading