diff --git a/gatsby-config.js b/gatsby-config.js index f62fbd952..132cbda6e 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -1,11 +1,34 @@ +const { assertTrailingSlash } = require('./src/utils/assert-trailing-slash'); +const { assertLeadingSlash } = require('./src/utils/assert-leading-slash'); const { generatePathPrefix } = require('./src/utils/generate-path-prefix'); const { siteMetadata } = require('./src/utils/site-metadata'); +const { findKeyValuePair } = require('./src/utils/find-key-value-pair'); const pathPrefix = generatePathPrefix(siteMetadata); const layoutComponentRelativePath = `./src/layouts/index.js`; console.log('PATH PREFIX', pathPrefix); +// TODO: move into separate ts util +function findPage(pages, id) { + return pages.find((p) => assertLeadingSlash(assertTrailingSlash(p.id)) === id); +} + +function generatePermutations(data) { + const keys = data.map((obj) => obj.value); + const values = data.map((obj) => obj.selections.map((sel) => sel.value)); + + // Generate Cartesian product + return values + .reduce( + (acc, curr) => { + return acc.flatMap((prev) => curr.map((value) => [...prev, value])); + }, + [[]] + ) + .map((combination) => Object.fromEntries(keys.map((key, i) => [key, combination[i]]))); +} + // Specifies which plugins to use depending on build environment // Keep our main plugin at top to include file saving before image plugins const plugins = [ @@ -21,7 +44,63 @@ const plugins = [ }, }, 'gatsby-plugin-emotion', - 'gatsby-plugin-sitemap', + { + resolve: 'gatsby-plugin-sitemap', + options: { + query: ` + { + allSitePage { + nodes { + path + pageContext + } + } + + allPage { + nodes { + id + ast + } + } + } + `, + resolveSiteUrl: () => siteMetadata.siteUrl, + resolvePages: ({ allSitePage: { nodes: sitePages }, allPage: { nodes: pages } }) => { + // console.log(sitePages); + const composableSitePages = sitePages.filter((p) => p?.pageContext?.options?.consumables); + + for (const composableSitePage of composableSitePages) { + const page = findPage(pages, composableSitePage.path); + if (!page) { + console.error(`Site Page with consumable reported at path ${composableSitePage.path}, but no page exists`); + continue; + } + + // find composable node. + const composableNode = findKeyValuePair(page.ast.children, 'name', 'composable-tutorial'); + if (!composableNode) { + console.error(`Composable node not found on page ${page.id}`); + continue; + } + + // construct query params + // TODO: this should be from children. not options + const permutations = generatePermutations(composableNode['composable-options']); + + for (const permutation of permutations) { + const queryString = new URLSearchParams(permutation).toString(); + sitePages.push({ + ...composableSitePage, + id: `${composableSitePage.id}?${queryString}`, + path: `${composableSitePage.path}?${queryString}`, + }); + } + } + + return sitePages; + }, + }, + }, { resolve: 'gatsby-plugin-layout', options: { diff --git a/package-lock.json b/package-lock.json index c8cfa8457..15df0da79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,6 +114,7 @@ "@testing-library/jest-dom": "^5.16.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.5.0", + "@types/gatsbyjs__reach-router": "^2.0.5", "@types/jest": "^29.5.14", "@types/node": "^22.10.1", "@types/react": "^18.3.13", @@ -10777,6 +10778,17 @@ "resolved": "https://artifactory.corp.mongodb.com/artifactory/api/npm/npm/@types/facepaint/-/facepaint-1.2.5.tgz", "integrity": "sha512-fi9kvwtC3IQ6Y3QVDkYEZsqEcetAdWD0zqqk8pEHlDXWkgS2WzolWN8Z5PGPT7YJ7ga71CCI0fVKVnVKqV+P6Q==" }, + "node_modules/@types/gatsbyjs__reach-router": { + "version": "2.0.5", + "resolved": "https://artifactory.corp.mongodb.com/artifactory/api/npm/npm/@types/gatsbyjs__reach-router/-/gatsbyjs__reach-router-2.0.5.tgz", + "integrity": "sha512-R5iFQM4FOrsxQdNFjtwSuHPnzQpPMdr9fGuWAmG417Lft/RZ4R5R0oNEz4nTbfS05i/TiLzNzlm0Ep2yYp+jcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/reach__router": "*", + "@types/react": "*" + } + }, "node_modules/@types/get-port": { "version": "3.2.0", "license": "MIT" @@ -39890,6 +39902,16 @@ "resolved": "https://artifactory.corp.mongodb.com/artifactory/api/npm/npm/@types/facepaint/-/facepaint-1.2.5.tgz", "integrity": "sha512-fi9kvwtC3IQ6Y3QVDkYEZsqEcetAdWD0zqqk8pEHlDXWkgS2WzolWN8Z5PGPT7YJ7ga71CCI0fVKVnVKqV+P6Q==" }, + "@types/gatsbyjs__reach-router": { + "version": "2.0.5", + "resolved": "https://artifactory.corp.mongodb.com/artifactory/api/npm/npm/@types/gatsbyjs__reach-router/-/gatsbyjs__reach-router-2.0.5.tgz", + "integrity": "sha512-R5iFQM4FOrsxQdNFjtwSuHPnzQpPMdr9fGuWAmG417Lft/RZ4R5R0oNEz4nTbfS05i/TiLzNzlm0Ep2yYp+jcA==", + "dev": true, + "requires": { + "@types/reach__router": "*", + "@types/react": "*" + } + }, "@types/get-port": { "version": "3.2.0" }, diff --git a/package.json b/package.json index 263ade5d3..8254b2035 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@testing-library/jest-dom": "^5.16.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.5.0", + "@types/gatsbyjs__reach-router": "^2.0.5", "@types/jest": "^29.5.14", "@types/node": "^22.10.1", "@types/react": "^18.3.13", diff --git a/plugins/gatsby-source-snooty-prod/gatsby-node.js b/plugins/gatsby-source-snooty-prod/gatsby-node.js index 121050edb..13a742225 100644 --- a/plugins/gatsby-source-snooty-prod/gatsby-node.js +++ b/plugins/gatsby-source-snooty-prod/gatsby-node.js @@ -21,6 +21,8 @@ const { createOpenAPIChangelogNode } = require('../utils/openapi.js'); const { createProductNodes } = require('../utils/products.js'); const { createDocsetNodes } = require('../utils/docsets.js'); const { createBreadcrumbNodes } = require('../utils/breadcrumbs.js'); +const TEST_PAGE_AST = require('../../tests/unit/data/Composable.test.json'); +const TEST_SLUG = 'composable-tutorial'; const assets = new Map(); const projectComponents = new Set(); @@ -189,6 +191,19 @@ exports.sourceNodes = async ({ actions, createContentDigest, createNodeId, getNo if (val?.ast?.options?.template === 'changelog') hasOpenAPIChangelog = true; }); + // TESTING PAGE WITH AST + createNode({ + id: TEST_SLUG, + page_id: TEST_SLUG, + ast: TEST_PAGE_AST.ast, + facets: [], + internal: { + type: 'Page', + contentDigest: createContentDigest(TEST_PAGE_AST), + }, + componentNames: projectComponents, + }); + await createDocsetNodes({ db, createNode, createNodeId, createContentDigest }); await createProductNodes({ db, createNode, createNodeId, createContentDigest }); @@ -347,6 +362,21 @@ exports.createPages = async ({ actions, graphql, reporter }) => { }); }); + // TESTING DOP-5476 + // TODO: remove before merge + createPage({ + path: assertTrailingSlash(TEST_SLUG), + component: path.resolve(__dirname, `../../src/components/DocumentBody.js`), + context: { + page_id: TEST_SLUG, + slug: TEST_SLUG, + repoBranches, + options: { + consumables: true, + }, + }, + }); + resolve(); }); }; diff --git a/src/components/ComponentFactory.js b/src/components/ComponentFactory.js index 61ee6643e..141e1d1b7 100644 --- a/src/components/ComponentFactory.js +++ b/src/components/ComponentFactory.js @@ -83,6 +83,7 @@ import RoleRed from './Roles/Red'; import RoleGold from './Roles/Gold'; import RoleRequired from './Roles/Required'; import SeeAlso from './SeeAlso'; +import { ComposableTutorial } from './ComposableTutorial'; const IGNORED_NAMES = new Set([ 'contents', @@ -139,6 +140,7 @@ const componentMap = { code: Code, collapsible: Collapsible, 'community-driver': CommunityPillLink, + 'composable-tutorial': ComposableTutorial, 'io-code-block': CodeIO, cond: Cond, container: Container, diff --git a/src/components/ComposableTutorial/Composable.tsx b/src/components/ComposableTutorial/Composable.tsx new file mode 100644 index 000000000..ca108d566 --- /dev/null +++ b/src/components/ComposableTutorial/Composable.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ComposableNode } from '../../types/ast'; +import ComponentFactory from '../ComponentFactory'; + +interface ComposableProps { + nodeData: ComposableNode; +} + +const Composable = ({ nodeData: { children }, ...rest }: ComposableProps) => { + return ( +
+ {children.map((c, i) => ( + + ))} +
+ ); +}; + +export default Composable; diff --git a/src/components/ComposableTutorial/ComposableTutorial.tsx b/src/components/ComposableTutorial/ComposableTutorial.tsx new file mode 100644 index 000000000..5753d0de3 --- /dev/null +++ b/src/components/ComposableTutorial/ComposableTutorial.tsx @@ -0,0 +1,258 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useLocation } from '@gatsbyjs/reach-router'; +import { parse, ParsedQuery } from 'query-string'; +import { navigate } from 'gatsby'; +import styled from '@emotion/styled'; +import { palette } from '@leafygreen-ui/palette'; +import { ComposableNode, ComposableTutorialNode } from '../../types/ast'; +import { getLocalValue, setLocalValue } from '../../utils/browser-storage'; +import { isBrowser } from '../../utils/is-browser'; +import { theme } from '../../theme/docsTheme'; +import Composable from './Composable'; +import ConfigurableOption from './ConfigurableOption'; + +// helper function to join key-value pairs as one string +// ordered by keys alphabetically +// separated by "." +export function joinKeyValuesAsString(targetObj: { [key: string]: string }) { + return Object.keys(targetObj) + .map((key) => `${key}=${targetObj[key]}`) + .sort() + .join('.'); +} + +function filterValidQueryParams( + parsedQuery: ParsedQuery, + composableOptions: ComposableTutorialNode['composable-options'], + validSelections: Set, + fallbackToDefaults = false +) { + const validQueryParams = composableOptions.reduce( + (res: Record[] }>, composableOption) => { + res[composableOption['value']] = { + values: composableOption.selections.map((s) => s.value), + dependencies: composableOption.dependencies, + }; + return res; + }, + {} + ); + + const res: Record = {}; + + // query params take precedence + for (const [key, value] of Object.entries(parsedQuery)) { + const dependenciesMet = validQueryParams[key].dependencies.every((d) => { + const key = Object.keys(d)[0]; + return parsedQuery[key] === Object.values(d)[0]; + }); + if ( + key in validQueryParams && + typeof value === 'string' && + validQueryParams[key]['values'].indexOf(value) > -1 && + dependenciesMet + ) { + res[key] = value; + } + } + + if (!fallbackToDefaults) { + return res; + } + + // fallback to composableOptions if not present in query + for (const composableOption of composableOptions) { + const dependenciesMet = composableOption.dependencies.every((d) => { + const key = Object.keys(d)[0]; + return res[key] === Object.values(d)[0]; + }); + + // skip this composable option if + // there is already a valid value in parsed query, + // or this option has missing dependency + if (res[composableOption.value] || !dependenciesMet) { + continue; + } + // check if default value for this option has content + const targetObj = { ...res, [composableOption.value]: composableOption.default }; + const targetString = joinKeyValuesAsString(targetObj); + if (validSelections.has(targetString)) { + res[composableOption.value] = composableOption.default; + continue; + } + + // if the specified default does not have content (fault in data) + // safety to find a valid combination from children and select + const currentSelections = joinKeyValuesAsString({...res}); + for (const [validSelection] of validSelections.entries()) { + const validSelectionParts = validSelection.split('.'); + const selectionPartForOption = validSelectionParts.find((str) => str.includes(`${composableOption.value}=`)); + if (validSelection.includes(currentSelections) && selectionPartForOption) { + res[composableOption.value] = selectionPartForOption.split('=')[1]; + } + } + } + + return res; +} + +function fulfilledSelections( + filteredParams: Record, + composableOptions: ComposableTutorialNode['composable-options'] +) { + // every composable option should either + // have its value as a key in selections + // or its dependency was not met + return composableOptions.every((composableOption) => { + const dependenciesMet = composableOption.dependencies.every((d) => { + const key = Object.keys(d)[0]; + return filteredParams[key] === Object.values(d)[0]; + }); + return composableOption['value'] in filteredParams || !dependenciesMet; + }); +} + +export function getSelectionPermutation(selections: Record[]): Set { + const res: Set = new Set(); + let partialRes: string[] = []; + for (const selection of selections) { + for (const [key, value] of Object.entries(selection)) { + partialRes.push(`${key}=${value}`); + res.add(partialRes.sort().join('.')); + } + } + return res; +} + +interface ComposableProps { + nodeData: ComposableTutorialNode; +} + +const LOCAL_STORAGE_KEY = 'activeComposables'; + +const ComposableContainer = styled.div` + display: flex; + position: sticky; + top: ${theme.header.actionBarMobileHeight}; + background: var(--background-color-primary); + column-gap: ${theme.size.default}; + row-gap: 12px; + justify-items: space-between; + border-bottom: 1px solid ${palette.gray.light2}; + padding-bottom: ${theme.size.medium}; +`; + +const ComposableTutorial = ({ + nodeData: { 'composable-options': composableOptions, children }, + ...rest +}: ComposableProps) => { + const [currentSelections, setCurrentSelections] = useState>(() => ({})); + const location = useLocation(); + + const validSelections = useMemo(() => { + let res: Set = new Set(); + for (const composableNode of children) { + const newSet = getSelectionPermutation(composableNode.options.selections); + for (const elm of newSet) { + res.add(elm); + } + } + return res; + }, [children]); + + // takes care of query param reading and rerouting + // if query params fulfill all selections, show the selections + // otherwise, fallback to getting default values from combination of local storage and node Data + useEffect(() => { + if (!isBrowser) { + return; + } + + // read query params + const queryParams = parse(location.search); + const filteredParams = filterValidQueryParams(queryParams, composableOptions, validSelections, false); + + // if params fulfill selections, show the current selections + if (fulfilledSelections(filteredParams, composableOptions)) { + setLocalValue(LOCAL_STORAGE_KEY, filteredParams); + setCurrentSelections(filteredParams); + return; + } + + // params are missing. get default values using local storage and nodeData + const localStorage: Record = getLocalValue(LOCAL_STORAGE_KEY) ?? {}; + const defaultParams = filterValidQueryParams(localStorage, composableOptions, validSelections, true); + const queryString = new URLSearchParams(defaultParams).toString(); + navigate(`?${queryString}`); + }, [composableOptions, location.pathname, location.search, validSelections]); + + const showComposable = useCallback( + (dependencies: { [key: string]: string }[]) => + dependencies.every((d) => { + const key = Object.keys(d)[0]; + const value = Object.values(d)[0]; + return currentSelections[key] === value; + }), + [currentSelections] + ); + + const onSelect = useCallback( + (value: string, option: string, index: number) => { + // the ones that occur less than index, take it + const newSelections = { ...currentSelections, [option]: value }; + + const targetString = joinKeyValuesAsString(newSelections); + + if (validSelections.has(targetString)) { + setCurrentSelections(currentSelections); + const queryString = new URLSearchParams(newSelections).toString(); + return navigate(`?${queryString}`); + } + + // need to correct preceding options + // keep selections for previous composable options + // and generate valid selections + const persistSelections: Record = { + [option]: value, + }; + for (let idx = 0; idx < index; idx++) { + const composableOption = composableOptions[idx]; + if (composableOption.value !== option && currentSelections[composableOption.value]) { + persistSelections[composableOption.value] = currentSelections[composableOption.value]; + } + } + + const defaultParams = filterValidQueryParams(persistSelections, composableOptions, validSelections, true); + const queryString = new URLSearchParams(defaultParams).toString(); + return navigate(`?${queryString}`); + }, + [composableOptions, currentSelections, validSelections] + ); + + return ( + <> + + {composableOptions.map((option, index) => ( + + ))} + + {children.map((c, i) => { + if (showComposable(c.options.selections)) { + return ; + } + return null; + })} + + ); +}; + +export default ComposableTutorial; diff --git a/src/components/ComposableTutorial/ConfigurableOption.tsx b/src/components/ComposableTutorial/ConfigurableOption.tsx new file mode 100644 index 000000000..106146b3d --- /dev/null +++ b/src/components/ComposableTutorial/ConfigurableOption.tsx @@ -0,0 +1,69 @@ +import React, { useMemo } from 'react'; +import { Option, Select } from '@leafygreen-ui/select'; +import { css, cx } from '@leafygreen-ui/emotion'; +import { ComposableTutorialOption } from '../../types/ast'; +import { joinKeyValuesAsString } from './ComposableTutorial'; + +const selectStyling = css` + flex: 1 0 auto; +`; + +interface ConfigurationOptionProps { + validSelections: Set; + option: ComposableTutorialOption; + selections: Record; + showComposable: (dependencies: Record[]) => boolean; + onSelect: (value: string, option: string, key: number) => void; + precedingOptions: ComposableTutorialOption[]; + optionIndex: number; +} + +const ConfigurableOption = ({ + option, + selections, + onSelect, + showComposable, + validSelections, + precedingOptions, + optionIndex, +}: ConfigurationOptionProps) => { + const filteredOptions = useMemo(() => { + return option.selections.filter((selection) => { + // find a validSelection whilst replacing the current configurable option with each options value + // if its valid, option is valid + const precedingSelections: Record = {}; + for (const precedingOption of precedingOptions) { + if (selections[precedingOption.value]) { + precedingSelections[precedingOption.value] = selections[precedingOption.value]; + } + } + const targetObj = { ...precedingSelections, [option.value]: selection.value }; + const targetString = joinKeyValuesAsString(targetObj); + + return validSelections.has(targetString); + }); + }, [option, precedingOptions, selections, validSelections]); + + if (!showComposable(option.dependencies)) { + return null; + } + + return ( + + ); +}; + +export default ConfigurableOption; diff --git a/src/components/ComposableTutorial/index.tsx b/src/components/ComposableTutorial/index.tsx new file mode 100644 index 000000000..d0a302d65 --- /dev/null +++ b/src/components/ComposableTutorial/index.tsx @@ -0,0 +1,3 @@ +import ComposableTutorial from './ComposableTutorial'; + +export { ComposableTutorial }; diff --git a/src/types/ast.ts b/src/types/ast.ts index 3b74bfe56..79a396af1 100644 --- a/src/types/ast.ts +++ b/src/types/ast.ts @@ -201,6 +201,32 @@ interface TocTreeDirective extends Directive { entries: TocTreeEntry[]; } +interface ComposableTutorialOption { + default: string; + dependencies: Record[]; + selections: { value: string; text: string }[]; + text: string; + value: string; +} + +interface ComposableTutorialNode extends Directive<{ 'composable-options': ComposableTutorialOption[] }> { + type: 'directive'; + name: 'composable-tutorials'; + children: ComposableNode[]; + 'composable-options': ComposableTutorialOption[]; +} + +interface ComposableNodeOption { + selections: { value: string; text: string }[]; +} + +interface ComposableNode extends Directive { + type: 'directive'; + name: 'composable'; + children: Directive[]; + options: ComposableNodeOption; +} + export type { NodeType, Node, @@ -235,4 +261,8 @@ export type { AdmonitionName, TocTreeEntry, TocTreeDirective, + ComposableNodeOption, + ComposableNode, + ComposableTutorialNode, + ComposableTutorialOption, }; diff --git a/tests/unit/data/Composable.test.json b/tests/unit/data/Composable.test.json new file mode 100644 index 000000000..a13e58b3e --- /dev/null +++ b/tests/unit/data/Composable.test.json @@ -0,0 +1,567 @@ +{ + "ast": { + "children": [ + { + "type": "root", + "position": { + "start": { + "line": { + "$numberInt": "0" + } + } + }, + "children": [ + { + "type": "section", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "children": [ + { + "type": "heading", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "children": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "value": "DOP-5476" + } + ], + "id": "DOP-5476" + }, + { + "type": "directive", + "position": { + "start": { + "line": { + "$numberInt": "173" + } + } + }, + "domain": "", + "name": "composable-tutorial", + "argument": [], + "children": [ + { + "type": "directive", + "position": { + "start": { + "line": { + "$numberInt": "173" + } + } + }, + "domain": "", + "name": "composable", + "argument": [], + "children": [ + { + "type": "heading", + "position": { + "start": { + "line": { + "$numberInt": "179" + } + } + }, + "children": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "value": "HEADING UNDER API REPLICATION AZURE" + } + ], + "id": "api-standalone-aws" + } + ], + "options": { + "selections": [ + { "interface": "api" }, + { "deployment-type": "replication" }, + { "cloud-provider": "azure" } + ] + } + }, + { + "type": "directive", + "position": { + "start": { + "line": { + "$numberInt": "173" + } + } + }, + "domain": "", + "name": "composable", + "argument": [], + "children": [ + { + "type": "heading", + "position": { + "start": { + "line": { + "$numberInt": "179" + } + } + }, + "children": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "value": "HEADING UNDER API STANDALONE GCP" + } + ], + "id": "api-standalone-aws" + } + ], + "options": { + "selections": [ + { "interface": "api" }, + { "deployment-type": "standalone" }, + { "cloud-provider": "gcp" } + ] + } + }, + { + "type": "directive", + "position": { + "start": { + "line": { + "$numberInt": "173" + } + } + }, + "domain": "", + "name": "composable", + "argument": [], + "children": [ + { + "type": "heading", + "position": { + "start": { + "line": { + "$numberInt": "179" + } + } + }, + "children": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "value": "HEADING UNDER API STANDALONE AZURE" + } + ], + "id": "api-standalone-aws" + } + ], + "options": { + "selections": [ + { "interface": "api" }, + { "deployment-type": "standalone" }, + { "cloud-provider": "azure" } + ] + } + }, + { + "type": "directive", + "position": { + "start": { + "line": { + "$numberInt": "173" + } + } + }, + "domain": "", + "name": "composable", + "argument": [], + "children": [ + { + "type": "heading", + "position": { + "start": { + "line": { + "$numberInt": "179" + } + } + }, + "children": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "value": "HEADING UNDER API REPLICATION AWS" + } + ], + "id": "api-standalone-aws" + } + ], + "options": { + "selections": [ + { "interface": "api" }, + { "deployment-type": "replication" }, + { "cloud-provider": "aws" } + ] + } + }, + { + "type": "directive", + "position": { + "start": { + "line": { + "$numberInt": "173" + } + } + }, + "domain": "", + "name": "composable", + "argument": [], + "children": [ + { + "type": "heading", + "position": { + "start": { + "line": { + "$numberInt": "179" + } + } + }, + "children": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "value": "HEADING UNDER API SHARDED AWS" + } + ], + "id": "api-standalone-aws" + } + ], + "options": { + "selections": [ + { "interface": "api" }, + { "deployment-type": "sharded" }, + { "cloud-provider": "aws" } + ] + } + }, + { + "type": "directive", + "position": { + "start": { + "line": { + "$numberInt": "173" + } + } + }, + "domain": "", + "name": "composable", + "argument": [], + "children": [ + { + "type": "heading", + "position": { + "start": { + "line": { + "$numberInt": "179" + } + } + }, + "children": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "value": "HEADING UNDER API SHARDED GCP" + } + ], + "id": "api-standalone-aws" + } + ], + "options": { + "selections": [ + { "interface": "api" }, + { "deployment-type": "sharded" }, + { "cloud-provider": "gcp" } + ] + } + }, + { + "type": "directive", + "position": { + "start": { + "line": { + "$numberInt": "173" + } + } + }, + "domain": "", + "name": "composable", + "argument": [], + "children": [ + { + "type": "heading", + "position": { + "start": { + "line": { + "$numberInt": "179" + } + } + }, + "children": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "8" + } + } + }, + "value": "HEADING UNDER DRIVER NODE REPLICATION AZURE" + } + ], + "id": "api-standalone-aws" + } + ], + "options": { + "selections": [ + { "interface": "driver" }, + { "language": "nodejs" }, + { "deployment-type": "replication" }, + { "cloud-provider": "azure" } + ] + } + } + ], + "composable-options": [ + { + "value": "interface", + "text": "Interface", + "default": "driver", + "dependencies": [], + "selections": [ + { + "value": "api", + "text": "API" + }, + { + "value": "cli", + "text": "CLI" + }, + { + "value": "compass", + "text": "Compass" + }, + { + "value": "driver", + "text": "Driver" + }, + { + "value": "ui", + "text": "Atlas UI" + } + ] + }, + { + "value": "language", + "text": "Language", + "default": "nodejs", + "dependencies": [{ "interface": "driver" }], + "selections": [ + { + "value": "c", + "text": "C" + }, + { + "value": "java-async", + "text": "Java (Async)" + }, + { + "value": "php", + "text": "PHP" + }, + { + "value": "nodejs", + "text": "Node.js" + } + ] + }, + { + "value": "deployment-type", + "text": "Deployment Type", + "default": "standalone", + "dependencies": [], + "selections": [ + { + "value": "standalone", + "text": "Standalone" + }, + { + "value": "replication", + "text": "Replication" + }, + { + "value": "sharded", + "text": "Sharded" + } + ] + }, + { + "value": "cloud-provider", + "text": "Cloud Provider", + "default": "aws", + "dependencies": [], + "selections": [ + { + "value": "aws", + "text": "AWS" + }, + { + "value": "gcp", + "text": "GCP" + }, + { + "value": "azure", + "text": "Azure" + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "root", + "fileid": "composable-tutorials.txt", + "options": { + "headings": [ + { + "depth": 1, + "id": "insert-documents-in-the-mongodb-atlas-ui", + "title": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "45" + } + } + }, + "value": "Insert Documents in the MongoDB Atlas UI" + } + ], + "selector_ids": { + "composable": [{ "drivers": "ui" }] + } + }, + { + "depth": 1, + "id": "insert-a-single-document", + "title": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "88" + } + } + }, + "value": "Insert a Single Document" + } + ], + "selector_ids": {} + }, + { + "depth": 1, + "id": "insert-multiple-documents", + "title": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "385" + } + } + }, + "value": "Insert Multiple Documents" + } + ], + "selector_ids": {} + }, + { + "depth": 1, + "id": "insert-behavior", + "title": [ + { + "type": "text", + "position": { + "start": { + "line": { + "$numberInt": "665" + } + } + }, + "value": "Insert Behavior" + } + ], + "selector_ids": {} + } + ], + "composables": true + } + } +}