diff --git a/packages/compass-collection/src/stores/collection-tab.ts b/packages/compass-collection/src/stores/collection-tab.ts index b0b10534fbb..991887b1fb4 100644 --- a/packages/compass-collection/src/stores/collection-tab.ts +++ b/packages/compass-collection/src/stores/collection-tab.ts @@ -80,6 +80,10 @@ export function activatePlugin( store.dispatch(selectTab('Aggregations')); }); + on(localAppRegistry, 'menu-share-schema-json', () => { + store.dispatch(selectTab('Schema')); + }); + void collectionModel.fetchMetadata({ dataService }).then((metadata) => { store.dispatch(collectionMetadataFetched(metadata)); }); diff --git a/packages/compass-schema/src/components/compass-schema.tsx b/packages/compass-schema/src/components/compass-schema.tsx index 8a466f03d20..b8f930b8e8d 100644 --- a/packages/compass-schema/src/components/compass-schema.tsx +++ b/packages/compass-schema/src/components/compass-schema.tsx @@ -39,6 +39,7 @@ import type { RootState } from '../stores/store'; import { startAnalysis, stopAnalysis } from '../stores/schema-analysis-reducer'; import { openExportSchema } from '../stores/schema-export-reducer'; import ExportSchemaModal from './export-schema-modal'; +import ExportSchemaLegacyBanner from './export-schema-legacy-banner'; const rootStyles = css({ width: '100%', @@ -431,6 +432,7 @@ const Schema: React.FunctionComponent<{ {enableExportSchema && } + {enableExportSchema && } ); }; diff --git a/packages/compass-schema/src/components/export-schema-legacy-banner.tsx b/packages/compass-schema/src/components/export-schema-legacy-banner.tsx new file mode 100644 index 00000000000..f46e3ed3779 --- /dev/null +++ b/packages/compass-schema/src/components/export-schema-legacy-banner.tsx @@ -0,0 +1,547 @@ +import React, { useCallback, useState } from 'react'; +import { connect } from 'react-redux'; +import { + ModalBody, + ModalHeader, + Modal, + css, + spacing, + Badge, + Button, + Checkbox, +} from '@mongodb-js/compass-components'; + +import type { RootState, SchemaThunkDispatch } from '../stores/store'; +import { + confirmedLegacySchemaShare, + switchToSchemaExport, + SchemaExportActions, + stopShowingLegacyBanner, +} from '../stores/schema-export-reducer'; + +const SchemaExportSVG = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +const imageContainerStyles = css({ + display: 'flex', + justifyContent: 'center', + marginBottom: spacing[200], +}); + +const containerStyles = css({ + padding: spacing[600], + width: '650px', +}); + +const checkboxContainerStyles = css({ + marginTop: spacing[300], +}); + +const comparisonContainerStyles = css({ + display: 'grid', + gridTemplateColumns: '1fr 1fr', + columnGap: spacing[900], + rowGap: spacing[400], + alignItems: 'flex-start', + justifyItems: 'flex-start', + marginTop: spacing[600], +}); + +const optionHeaderStyles = css({ + fontSize: spacing[400], + margin: '0px', +}); + +const ExportSchemaLegacyBanner: React.FunctionComponent<{ + isOpen: boolean; + onClose: () => void; + onLegacyShare: () => void; + onSwitchToSchemaExport: () => void; + stopShowingLegacyBanner: (choice: 'legacy' | 'export') => void; +}> = ({ + isOpen, + onClose, + onLegacyShare, + onSwitchToSchemaExport, + stopShowingLegacyBanner, +}) => { + const [dontShowAgainChecked, setDontShowAgainChecked] = useState(false); + const handleLegacyShare = useCallback(() => { + if (dontShowAgainChecked) stopShowingLegacyBanner('legacy'); + onLegacyShare(); + }, [onLegacyShare, dontShowAgainChecked, stopShowingLegacyBanner]); + const handleSwitchToNew = useCallback(() => { + if (dontShowAgainChecked) stopShowingLegacyBanner('export'); + onSwitchToSchemaExport(); + }, [onSwitchToSchemaExport, dontShowAgainChecked, stopShowingLegacyBanner]); + return ( + + +
+ +
+ New & Improved Export Schema Experience + + } + subtitle={` + Try the new Export Schema to generate your collection schema in multiple formats. + The previous 'Share Schema' experience will not be receiving future updates moving forward. + `} + /> + +
+ Legacy + New +

Share JSON Schema

+

Export JSON Schema

+
+ Non-standard schema format without customization capabilities. +
+
+ 3 standardized schema formats designed for schema validation and + analysis use cases. +
+ + +
+
+ setDontShowAgainChecked(e.currentTarget.checked)} + /> +
+
+
+ ); +}; + +export default connect( + (state: RootState) => ({ + isOpen: state.schemaExport.isLegacyBannerOpen, + }), + (dispatch: SchemaThunkDispatch) => ({ + onClose: () => dispatch({ type: SchemaExportActions.closeLegacyBanner }), + onLegacyShare: () => dispatch(confirmedLegacySchemaShare()), + onSwitchToSchemaExport: () => dispatch(switchToSchemaExport()), + stopShowingLegacyBanner: (choice: 'legacy' | 'export') => + dispatch(stopShowingLegacyBanner(choice)), + }) +)(ExportSchemaLegacyBanner); diff --git a/packages/compass-schema/src/stores/schema-analysis-reducer.ts b/packages/compass-schema/src/stores/schema-analysis-reducer.ts index 21e7b74d469..c6a5648d4f5 100644 --- a/packages/compass-schema/src/stores/schema-analysis-reducer.ts +++ b/packages/compass-schema/src/stores/schema-analysis-reducer.ts @@ -18,7 +18,6 @@ import { schemaContainsGeoData, } from '../modules/schema-analysis'; import { capMaxTimeMSAtPreferenceLimit } from 'compass-preferences-model/provider'; -import { openToast } from '@mongodb-js/compass-components'; import type { Circle, Layer, LayerGroup, Polygon } from 'leaflet'; import { mongoLogId } from '@mongodb-js/compass-logging/provider'; import type { SchemaThunkAction } from './store'; @@ -126,56 +125,6 @@ function resultId(): string { return new UUID().toString(); } -export const handleSchemaShare = (): SchemaThunkAction => { - return (dispatch, getState, { namespace }) => { - const { - schemaAnalysis: { schema }, - } = getState(); - const hasSchema = schema !== null; - if (hasSchema) { - void navigator.clipboard.writeText(JSON.stringify(schema, null, ' ')); - } - dispatch(_trackSchemaShared(hasSchema)); - openToast( - 'share-schema', - hasSchema - ? { - variant: 'success', - title: 'Schema Copied', - description: `The schema definition of ${namespace} has been copied to your clipboard in JSON format.`, - timeout: 5_000, - } - : { - variant: 'warning', - title: 'Analyze Schema First', - description: 'Please Analyze the Schema First from the Schema Tab.', - timeout: 5_000, - } - ); - }; -}; - -export const _trackSchemaShared = ( - hasSchema: boolean -): SchemaThunkAction => { - return (dispatch, getState, { track, connectionInfoRef }) => { - const { - schemaAnalysis: { schema }, - } = getState(); - // Use a function here to a) ensure that the calculations here - // are only made when telemetry is enabled and b) that errors from - // those calculations are caught and logged rather than displayed to - // users as errors from the core schema sharing logic. - const trackEvent = () => ({ - has_schema: hasSchema, - schema_width: schema?.fields?.length ?? 0, - schema_depth: schema ? calculateSchemaDepth(schema) : 0, - geo_data: schema ? schemaContainsGeoData(schema) : false, - }); - track('Schema Exported', trackEvent, connectionInfoRef.current); - }; -}; - const getInitialState = (): SchemaAnalysisState => ({ analysisState: ANALYSIS_STATE_INITIAL, errorMessage: '', diff --git a/packages/compass-schema/src/stores/schema-export-reducer.ts b/packages/compass-schema/src/stores/schema-export-reducer.ts index 7c8b4147d2c..ddb1e6c925f 100644 --- a/packages/compass-schema/src/stores/schema-export-reducer.ts +++ b/packages/compass-schema/src/stores/schema-export-reducer.ts @@ -9,7 +9,12 @@ import type { import type { SchemaThunkAction } from './store'; import { isAction } from '../utils'; -import type { SchemaAccessor } from '../modules/schema-analysis'; +import { + calculateSchemaDepth, + schemaContainsGeoData, + type SchemaAccessor, +} from '../modules/schema-analysis'; +import { openToast } from '@mongodb-js/compass-components'; export type SchemaFormat = | 'standardJSON' @@ -20,6 +25,8 @@ export type ExportStatus = 'inprogress' | 'complete' | 'error'; export type SchemaExportState = { abortController?: AbortController; isOpen: boolean; + isLegacyBannerOpen: boolean; + legacyBannerChoice?: 'legacy' | 'export'; exportedSchema?: string; exportFormat: SchemaFormat; errorMessage?: string; @@ -34,11 +41,16 @@ const getInitialState = (): SchemaExportState => ({ exportStatus: 'inprogress', exportedSchema: undefined, isOpen: false, + isLegacyBannerOpen: false, + legacyBannerChoice: undefined, }); export const enum SchemaExportActions { openExportSchema = 'schema-service/schema-export/openExportSchema', closeExportSchema = 'schema-service/schema-export/closeExportSchema', + openLegacyBanner = 'schema-service/schema-export/openLegacyBanner', + closeLegacyBanner = 'schema-service/schema-export/closeLegacyBanner', + setLegacyBannerChoice = 'schema-service/schema-export/setLegacyBannerChoice', changeExportSchemaStatus = 'schema-service/schema-export/changeExportSchemaStatus', changeExportSchemaFormatStarted = 'schema-service/schema-export/changeExportSchemaFormatStarted', changeExportSchemaFormatComplete = 'schema-service/schema-export/changeExportSchemaFormatComplete', @@ -263,6 +275,42 @@ export const schemaExportReducer: Reducer = ( }; } + if ( + isAction( + action, + SchemaExportActions.openLegacyBanner + ) + ) { + return { + ...state, + isLegacyBannerOpen: true, + }; + } + + if ( + isAction( + action, + SchemaExportActions.closeLegacyBanner + ) + ) { + return { + ...state, + isLegacyBannerOpen: false, + }; + } + + if ( + isAction( + action, + SchemaExportActions.setLegacyBannerChoice + ) + ) { + return { + ...state, + legacyBannerChoice: action.choice, + }; + } + if ( isAction( action, @@ -319,3 +367,110 @@ export const schemaExportReducer: Reducer = ( return state; }; + +// TODO clean out when phase out is confirmed COMPASS-8692 +export type openLegacyBannerAction = { + type: SchemaExportActions.openLegacyBanner; +}; + +export const openLegacyBanner = (): SchemaThunkAction => { + return (dispatch, getState) => { + const choiceInState = getState().schemaExport.legacyBannerChoice; + const savedChoice = choiceInState || localStorage.getItem(localStorageId); + if (savedChoice) { + if (savedChoice !== choiceInState) { + dispatch({ + type: SchemaExportActions.setLegacyBannerChoice, + choice: savedChoice, + }); + } + if (savedChoice === 'legacy') { + dispatch(confirmedLegacySchemaShare()); + return; + } + if (savedChoice === 'export') { + dispatch(openExportSchema()); + return; + } + } + dispatch({ type: SchemaExportActions.openLegacyBanner }); + }; +}; + +export type closeLegacyBannerAction = { + type: SchemaExportActions.closeLegacyBanner; +}; + +export type setLegacyBannerChoiceAction = { + type: SchemaExportActions.setLegacyBannerChoice; + choice: 'legacy' | 'export'; +}; + +const localStorageId = 'schemaExportLegacyBannerChoice'; + +export const switchToSchemaExport = (): SchemaThunkAction => { + return (dispatch) => { + dispatch({ type: SchemaExportActions.closeLegacyBanner }); + dispatch(openExportSchema()); + }; +}; + +export const confirmedLegacySchemaShare = (): SchemaThunkAction => { + return (dispatch, getState, { namespace }) => { + const { + schemaAnalysis: { schema }, + } = getState(); + const hasSchema = schema !== null; + if (hasSchema) { + void navigator.clipboard.writeText(JSON.stringify(schema, null, ' ')); + } + dispatch(_trackSchemaShared(hasSchema)); + dispatch({ type: SchemaExportActions.closeLegacyBanner }); + openToast( + 'share-schema', + hasSchema + ? { + variant: 'success', + title: 'Schema Copied', + description: `The schema definition of ${namespace} has been copied to your clipboard in JSON format.`, + timeout: 5_000, + } + : { + variant: 'warning', + title: 'Analyze Schema First', + description: + 'Please analyze the schema in the schema tab before sharing the schema.', + } + ); + }; +}; + +export const _trackSchemaShared = ( + hasSchema: boolean +): SchemaThunkAction => { + return (dispatch, getState, { track, connectionInfoRef }) => { + const { + schemaAnalysis: { schema }, + } = getState(); + // Use a function here to a) ensure that the calculations here + // are only made when telemetry is enabled and b) that errors from + // those calculations are caught and logged rather than displayed to + // users as errors from the core schema sharing logic. + const trackEvent = () => ({ + has_schema: hasSchema, + schema_width: schema?.fields?.length ?? 0, + schema_depth: schema ? calculateSchemaDepth(schema) : 0, + geo_data: schema ? schemaContainsGeoData(schema) : false, + }); + track('Schema Exported', trackEvent, connectionInfoRef.current); + }; +}; + +export const stopShowingLegacyBanner = ( + choice: 'legacy' | 'export' +): SchemaThunkAction => { + return (dispatch) => { + localStorage.setItem(localStorageId, choice); + dispatch({ type: SchemaExportActions.setLegacyBannerChoice, choice }); + }; +}; diff --git a/packages/compass-schema/src/stores/store.ts b/packages/compass-schema/src/stores/store.ts index d7efe6b7ab2..2a7bcf4bddc 100644 --- a/packages/compass-schema/src/stores/store.ts +++ b/packages/compass-schema/src/stores/store.ts @@ -17,13 +17,11 @@ import type { PreferencesAccess } from 'compass-preferences-model/provider'; import type { FieldStoreService } from '@mongodb-js/compass-field-store'; import type { QueryBarService } from '@mongodb-js/compass-query-bar'; import type { TrackFunction } from '@mongodb-js/compass-telemetry'; -import { - schemaAnalysisReducer, - handleSchemaShare, - stopAnalysis, -} from './schema-analysis-reducer'; +import { schemaAnalysisReducer, stopAnalysis } from './schema-analysis-reducer'; import { cancelExportSchema, + confirmedLegacySchemaShare, + openLegacyBanner, schemaExportReducer, } from './schema-export-reducer'; import type { InternalLayer } from '../modules/geo'; @@ -78,9 +76,14 @@ export function activateSchemaPlugin( * When `Share Schema as JSON` clicked in menu show a dialog message. */ - on(services.localAppRegistry, 'menu-share-schema-json', () => - store.dispatch(handleSchemaShare()) - ); + on(services.localAppRegistry, 'menu-share-schema-json', () => { + const { enableExportSchema } = services.preferences.getPreferences(); + if (enableExportSchema) { + store.dispatch(openLegacyBanner()); + return; + } + store.dispatch(confirmedLegacySchemaShare()); + }); addCleanup(() => store.dispatch(stopAnalysis())); addCleanup(() => store.dispatch(cancelExportSchema())); diff --git a/packages/compass/src/main/menu.spec.ts b/packages/compass/src/main/menu.spec.ts index 0b12470abd5..defb4fda95e 100644 --- a/packages/compass/src/main/menu.spec.ts +++ b/packages/compass/src/main/menu.spec.ts @@ -411,7 +411,7 @@ describe('CompassMenu', function () { submenu: [ { accelerator: 'Alt+CmdOrCtrl+S', - label: '&Share Schema as JSON', + label: '&Share Schema as JSON (Legacy)', }, { type: 'separator', @@ -445,7 +445,7 @@ describe('CompassMenu', function () { submenu: [ { accelerator: 'Alt+CmdOrCtrl+S', - label: '&Share Schema as JSON', + label: '&Share Schema as JSON (Legacy)', }, { type: 'separator', diff --git a/packages/compass/src/main/menu.ts b/packages/compass/src/main/menu.ts index 535ed2ea4f2..813dbe92d04 100644 --- a/packages/compass/src/main/menu.ts +++ b/packages/compass/src/main/menu.ts @@ -326,7 +326,7 @@ function collectionSubMenu( ): MenuItemConstructorOptions { const subMenu = []; subMenu.push({ - label: '&Share Schema as JSON', + label: '&Share Schema as JSON (Legacy)', accelerator: 'Alt+CmdOrCtrl+S', click() { ipcMain?.broadcastFocused('window:menu-share-schema-json');