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
+ }
+ }
+}