diff --git a/app/components/Markdown.tsx b/app/components/Markdown.tsx index 3849e3a..a4f48a3 100644 --- a/app/components/Markdown.tsx +++ b/app/components/Markdown.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; import { useMemo } from 'react'; import type { ReactElement } from 'react'; import Code from './Code'; -import { kebabCase } from 'change-case'; +import { headingAnchor } from '../lib/docsAnchors'; import { resolveMarkdownLink } from '../lib/markdownLinks'; interface MarkdownProps { @@ -43,7 +43,7 @@ export default function Markdown({ content, sourcePath }: MarkdownProps) { elements.push(
- +

{title}

diff --git a/app/components/QuickStartTabs.tsx b/app/components/QuickStartTabs.tsx new file mode 100644 index 0000000..97e1f1b --- /dev/null +++ b/app/components/QuickStartTabs.tsx @@ -0,0 +1,182 @@ +"use client"; + +import Link from "next/link"; +import { useRef, useState } from "react"; +import CommandSnippet from "./CommandSnippet"; + +type QuickStartTabsProps = { + dockerCommand: string; + vscodeDeepLinkUrl: string; + vscodeMarketplaceUrl: string; + dockerDocsUrl: string; + vscodeDocsUrl: string; +}; + +const TABS = [ + { id: "command", label: "Docker command", description: "Run it yourself" }, + { id: "guided", label: "Guided setup", description: "VS Code extension" }, +] as const; + +type TabId = (typeof TABS)[number]["id"]; + +export default function QuickStartTabs({ + dockerCommand, + vscodeDeepLinkUrl, + vscodeMarketplaceUrl, + dockerDocsUrl, + vscodeDocsUrl, +}: QuickStartTabsProps) { + const [activeTab, setActiveTab] = useState("command"); + const tabRefs = useRef>>({}); + + const selectTab = (id: TabId) => { + setActiveTab(id); + tabRefs.current[id]?.focus(); + }; + + const onTabKeyDown = (event: React.KeyboardEvent) => { + const currentIndex = TABS.findIndex((tab) => tab.id === activeTab); + + switch (event.key) { + case "ArrowRight": + case "ArrowLeft": { + event.preventDefault(); + const delta = event.key === "ArrowRight" ? 1 : -1; + selectTab(TABS[(currentIndex + delta + TABS.length) % TABS.length].id); + break; + } + case "Home": + event.preventDefault(); + selectTab(TABS[0].id); + break; + case "End": + event.preventDefault(); + selectTab(TABS[TABS.length - 1].id); + break; + default: + break; + } + }; + + return ( +
+
+ {TABS.map((tab) => { + const isActive = tab.id === activeTab; + + return ( + + ); + })} +
+ + + +
+
+ ); +} diff --git a/app/lib/docsAnchors.ts b/app/lib/docsAnchors.ts new file mode 100644 index 0000000..d86ea93 --- /dev/null +++ b/app/lib/docsAnchors.ts @@ -0,0 +1,12 @@ +import { kebabCase } from 'change-case'; + +/** + * Anchor id for a guide H2, exactly as Markdown.tsx emits it. Anything that links into a + * guide section derives the fragment here, so the renderer and the link cannot disagree. + */ +export function headingAnchor(title: string): string { + return kebabCase(title); +} + +export const vscodeExistingConnectionSectionTitle = 'Connect an existing instance'; +export const vscodeExistingConnectionSectionAnchor = headingAnchor(vscodeExistingConnectionSectionTitle); diff --git a/app/page.tsx b/app/page.tsx index a8a7098..ccd5d03 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,11 @@ import Image from "next/image"; import Link from "next/link"; -import CommandSnippet from "./components/CommandSnippet"; -import { documentdbKubernetesOperatorQuickStartUrl } from "./services/externalLinks"; +import QuickStartTabs from "./components/QuickStartTabs"; +import { + documentdbKubernetesOperatorQuickStartUrl, + documentdbVsCodeExtensionMarketplaceUrl, + documentdbVsCodeLocalQuickStartDeepLink, +} from "./services/externalLinks"; import { getMetadata } from "./services/metadataService"; import { documentdbGitHubForks, @@ -34,25 +38,10 @@ type Capability = { }; const quickRunCommand = `docker run -dt --name documentdb \\ - -p 10260:10260 \\ + -p 127.0.0.1:10260:10260 \\ ghcr.io/documentdb/documentdb/documentdb-local:latest \\ - --username \\ - --password `; - -const quickStartSteps = [ - { - step: "01", - description: "Run DocumentDB Local with Docker.", - }, - { - step: "02", - description: "Connect on port 10260 with your app, shell, or client.", - }, - { - step: "03", - description: "Continue with the docs or Linux packages for the setup you need.", - }, -]; + --username '' \\ + --password ''`; const kubernetesOperatorEntryPoints = [ { @@ -316,7 +305,7 @@ export default function Home() {
-
+

Open source document database @@ -372,43 +361,28 @@ export default function Home() { Quick start

- Run locally with Docker + Run DocumentDB locally

- Start DocumentDB Local with Docker, then connect on port - 10260. + Both options require Docker.

- -
    - {quickStartSteps.map((item) => ( -
  1. - - {item.step} - -

    - {item.description} -

    -
  2. - ))} -
-
- - Docker quick start - + +

+ Prefer to install directly on Linux?{" "} - Download packages + Linux packages guide -

+

diff --git a/app/services/articleService.ts b/app/services/articleService.ts index 2be8647..e499ce1 100644 --- a/app/services/articleService.ts +++ b/app/services/articleService.ts @@ -1,5 +1,9 @@ import fs from 'fs'; import path from 'path'; +import { + vscodeExistingConnectionSectionAnchor, + vscodeExistingConnectionSectionTitle, +} from '../lib/docsAnchors'; import { load as loadYaml } from 'js-yaml'; import matter from 'gray-matter'; import { Article } from '../types/Article'; @@ -616,15 +620,21 @@ If the target already has PostgreSQL, the PGDG extension dependencies (\`postgre const vscodeQuickStartGuideContent = `# Visual Studio Code Quick Start -Use DocumentDB for VS Code to connect to a local DocumentDB instance, browse sample data, and create your first database without leaving the editor. +Use DocumentDB for VS Code to set up a local DocumentDB instance, browse sample data, and create your first database without leaving the editor. + +The extension can create the instance for you: it pulls the official image, creates a container and persistent data volume, generates credentials, waits until the database accepts connections, and saves the connection. It does not install Docker. + +Already running DocumentDB? Skip provisioning and [connect your existing instance](#${vscodeExistingConnectionSectionAnchor}). ## Prerequisites - [Visual Studio Code](https://code.visualstudio.com/) - The [DocumentDB for VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-documentdb) -- A local DocumentDB instance from [Docker Quick Start](/docs/getting-started/docker) or a host setup with a running DocumentDB gateway +- Docker Desktop or Docker Engine, set to Linux containers, running wherever VS Code is - Optional: [mongosh](https://www.mongodb.com/docs/mongodb-shell/install/) for independent connection checks +Docker must be reachable from the environment VS Code runs in. If you work in WSL, a dev container, an SSH remote, or Codespaces, Docker needs to be available there rather than only on your host machine. Setup runs a readiness check and explains what to fix if it cannot reach Docker. + ## Install the extension Install the extension from the VS Code marketplace, or run: @@ -635,9 +645,28 @@ code --install-extension ms-azuretools.vscode-documentdb If VS Code prompts you to reload after installation, do that before creating a connection. -## Start DocumentDB first +## Set up DocumentDB Local -For the fastest local setup, start DocumentDB Local with Docker: +Use guided setup to let the extension provision DocumentDB Local and save its connection. There are no Docker commands for you to run. + +1. Open setup using any of these: + - Select the DocumentDB icon in the activity bar, expand **Your own DocumentDB** in the Connections view, and select **Set up DocumentDB Local**. + - Run **DocumentDB: Set up DocumentDB Local** from the Command Palette. + - Open \`vscode://ms-azuretools.vscode-documentdb/local\` from your browser and confirm the prompts. If the extension is not installed, VS Code offers to install it first. This needs extension version 0.10.1 or later. +2. On the **Introduction** step, select **Continue**. Nothing is downloaded or created until the next step. +3. On the **Configure** step, review the defaults and select **Start DocumentDB Local**. The defaults give you an available port (starting at \`10260\`), generated credentials, the \`latest\` official image, and optional sample data. Expand the advanced options to set the port, image tag, or credentials yourself. +4. Wait for setup to finish. The extension creates a container named \`vscode-documentdb-local\` with a persistent volume, then waits until the database accepts connections. +5. Select **Open Connection** to reveal the saved connection, then expand it to browse databases and collections. + +Sample data is enabled by default. If you keep it enabled, expand the saved connection to browse the sample database and collections. + +Right-click the DocumentDB Local entry to **Start**, **Stop**, **Restart**, or **Delete Container**, and to **Copy Connection String**, **Copy Password**, or **View Logs**. Stopping and starting preserves your data; deleting removes the volume and the generated credentials permanently. + +## Alternative: start the container yourself + +Use this if you want to manage the container yourself. If DocumentDB is already running, skip this step and [connect your existing instance](#${vscodeExistingConnectionSectionAnchor}). + +Start it with Docker: \`\`\`bash docker run -dt --name documentdb \\ @@ -649,11 +678,13 @@ docker run -dt --name documentdb \\ If you prefer a host installation instead of Docker, use the [Linux Packages Quick Start](/docs/getting-started/packages) on a distribution in the current release matrix. -## Add a local connection in VS Code +## ${vscodeExistingConnectionSectionTitle} + +Use this for a DocumentDB instance that is already running. You only add a connection; you do not need to run the setup wizard or create another container. Have the instance's port, username, and password ready. 1. Open the **DocumentDB** view in the VS Code activity bar. 2. In the local connection area, select **DocumentDB Local** and start the **New Local Connection** flow. -3. Enter port \`10260\`, your username, and your password. +3. Enter your instance's port (\`10260\` for the command above), username, and password. 4. At the TLS/SSL prompt: - Choose **Disable TLS/SSL (Not recommended)** if you are using the default self-signed local setup and have not configured trust for the certificate yet. - Keep **Enable TLS/SSL (Default)** if you already configured a trusted local certificate. @@ -661,12 +692,11 @@ If you prefer a host installation instead of Docker, use the [Linux Packages Qui ## Verify the connection in the extension -Once connected: +Guided setup loads sample data by default unless you turn that option off. The manual Docker command above starts without sample data; the [Docker Quick Start](/docs/getting-started/docker) shows how to enable it. -1. Expand the connection and open \`StoreData\`. This exists only if you started the container with \`--init-data true\`; without it DocumentDB Local starts empty. -2. Open the \`stores\` or \`ratings\` collection. -3. Switch between the **Table**, **Tree**, and **JSON** views to confirm the extension is reading data correctly. -4. Create your own database and collection from the context menu, then add a test document like: +1. Expand your saved connection. If sample data was loaded, open a sample database and collection to browse the documents. +2. Create your own database and collection from the context menu. An empty instance is expected when sample data is disabled. +3. In your own collection, add a test document like: \`\`\`json { @@ -676,6 +706,8 @@ Once connected: } \`\`\` +Switch between the **Table**, **Tree**, and **JSON** views to confirm the extension can read the document. + If you prefer to validate outside the extension first, use [Mongo Shell Quick Start](/docs/getting-started/mongo-shell-quickstart). ## Import, export, and querying @@ -689,8 +721,11 @@ After the connection works, the extension can help you continue without leaving ## Troubleshooting and debugging -If the extension does not connect on the first try: +If setup or the connection does not work on the first try: +- If the browser link does nothing, confirm the extension is installed and up to date, then run **DocumentDB: Set up DocumentDB Local** from the Command Palette instead +- If VS Code reports **No extension gallery service configured**, it could not reach a marketplace to install the extension for you. On managed devices that use a private marketplace, this can happen when the link is also what starts VS Code. Open the link again once VS Code has loaded, or install the extension yourself with \`code --install-extension ms-azuretools.vscode-documentdb\` and then open the link again +- If setup reports that Docker is unreachable, fix what it names (Docker not running, or Docker set to Windows containers rather than Linux) and select **Continue setup**; nothing has been created at that point - Verify the extension is installed and reload VS Code if the DocumentDB view does not appear - Confirm your local DocumentDB instance is actually running before you connect - If you used Docker, check \`docker ps\` and \`docker logs documentdb\` diff --git a/app/services/externalLinks.ts b/app/services/externalLinks.ts index 9445af0..7a3ada9 100644 --- a/app/services/externalLinks.ts +++ b/app/services/externalLinks.ts @@ -13,3 +13,9 @@ export const documentdbKubernetesOperatorQuickStartUrl = export const documentdbKubernetesOperatorGitHubUrl = 'https://github.com/documentdb/documentdb-kubernetes-operator'; + +export const documentdbVsCodeExtensionMarketplaceUrl = + 'https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-documentdb'; + +export const documentdbVsCodeLocalQuickStartDeepLink = + 'vscode://ms-azuretools.vscode-documentdb/local'; diff --git a/tests/quickStart.test.ts b/tests/quickStart.test.ts new file mode 100644 index 0000000..202c9ae --- /dev/null +++ b/tests/quickStart.test.ts @@ -0,0 +1,245 @@ +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; +import Home from '../app/page'; +import Markdown from '../app/components/Markdown'; +import { + headingAnchor, + vscodeExistingConnectionSectionAnchor, + vscodeExistingConnectionSectionTitle, +} from '../app/lib/docsAnchors'; +import { getArticleByPath } from '../app/services/articleService'; +import { + documentdbVsCodeExtensionMarketplaceUrl, + documentdbVsCodeLocalQuickStartDeepLink, +} from '../app/services/externalLinks'; + +const html = renderToStaticMarkup(createElement(Home)); +const cardStart = html.indexOf('id="run-with-docker"'); +const card = html.slice(cardStart, html.indexOf('
', cardStart)); +const vscodeGuideUrl = '/docs/getting-started/vscode-quickstart'; +const existingConnectionUrl = `${vscodeGuideUrl}#${vscodeExistingConnectionSectionAnchor}`; + +function panel(id: 'command' | 'guided') { + const start = card.indexOf(`id="quickstart-panel-${id}"`); + const end = id === 'command' + ? card.indexOf('id="quickstart-panel-guided"', start + 1) + : card.length; + expect(start).toBeGreaterThan(-1); + expect(end).toBeGreaterThan(start); + return card.slice(start, end); +} + +const guide = getArticleByPath('getting-started', ['vscode-quickstart']); +if (!guide) { + throw new Error('The VS Code quick-start guide must exist'); +} +const guideContent = guide.content; +const guideHtml = renderToStaticMarkup( + createElement(Markdown, { + content: guideContent, + sourcePath: 'getting-started/vscode-quickstart.md', + }), +); + +describe('homepage local quick start', () => { + it('preserves the public anchor and selects the Docker command by default', () => { + expect(cardStart).toBeGreaterThan(-1); + + for (const id of ['command', 'guided']) { + const selected = id === 'command'; + const tab = card.match( + new RegExp(`]*id="quickstart-tab-${id}"[^>]*>`), + )?.[0]; + expect(tab).toBeDefined(); + expect(tab).toContain(`aria-selected="${selected}"`); + expect(tab).toContain(`aria-controls="quickstart-panel-${id}"`); + expect(tab).toContain(`tabindex="${selected ? 0 : -1}"`); + + const panelTag = card.match( + new RegExp(`]*id="quickstart-panel-${id}"[^>]*>`), + )?.[0]; + expect(panelTag).toContain('role="tabpanel"'); + expect(panelTag).toContain(`aria-labelledby="quickstart-tab-${id}"`); + if (selected) { + expect(panelTag).not.toContain('hidden=""'); + } else { + expect(panelTag).toContain('hidden=""'); + } + } + }); + + it('preserves the loopback binding, quoted placeholders, and official image', () => { + const command = panel('command'); + expect(command).toContain('-p 127.0.0.1:10260:10260'); + expect(command).not.toContain('-p 10260:10260 '); + expect(command).toContain("--username '<YOUR_USERNAME>'"); + expect(command).toContain("--password '<YOUR_PASSWORD>'"); + expect(command).toContain('ghcr.io/documentdb/documentdb/documentdb-local:latest'); + expect(command).not.toContain('--init-data'); + }); + + it('labels setup workflows and identifies the extension before selection', () => { + const selector = card.slice( + card.indexOf('role="tablist"'), + card.indexOf('role="tabpanel"'), + ); + expect(selector).toContain('Docker command'); + expect(selector).toContain('Run it yourself'); + expect(selector).toContain('Guided setup'); + expect(selector).toContain('VS Code extension'); + expect(selector).not.toContain('>Terminal<'); + expect(selector).not.toContain('>VS Code<'); + expect(card).not.toContain('Want a GUI?'); + expect(card).not.toContain('connects to this container too'); + }); + + it('states the shared Docker requirement once, outside either panel', () => { + const requirement = 'Both options require Docker.'; + expect(card.split(requirement)).toHaveLength(2); + expect(card.indexOf(requirement)).toBeLessThan(card.indexOf('role="tablist"')); + expect(panel('command')).not.toContain(requirement); + expect(panel('guided')).not.toContain(requirement); + expect(panel('guided')).not.toContain('Linux containers'); + }); + + it('opens the Linux packages guide from below both panels, not before the default path', () => { + const href = 'href="/docs/getting-started/packages"'; + const link = card.indexOf(href); + expect(card.split(href)).toHaveLength(2); + expect(link).toBeGreaterThan(card.indexOf('id="quickstart-panel-guided"')); + // Closes the guided panel and the tabs wrapper, so the footer is visible from either tab. + expect(card.slice(0, card.lastIndexOf('<\/div>$/); + expect(card.slice(link)).toMatch(/^href="[^"]+"[^>]*>Linux packages guide { + expect(card).toContain('Run it yourself'); + expect(panel('guided')).toContain( + 'creates your local database, generates credentials, and saves a connection', + ); + expect(card).not.toContain('smoothest experience'); + expect(card).not.toContain('One click'); + expect(card).not.toContain('300 MB'); + }); + + it('offers one guided launch action with an honest editor and extension caption', () => { + expect(documentdbVsCodeLocalQuickStartDeepLink).toBe( + 'vscode://ms-azuretools.vscode-documentdb/local', + ); + const guided = panel('guided'); + const launch = guided.match( + /]*>Set up in VS Code<\/a>/, + )?.[0]; + expect(launch).toContain(`href="${documentdbVsCodeLocalQuickStartDeepLink}"`); + expect(launch).toContain('aria-describedby="quickstart-vscode-setup-caption"'); + expect(card.split(`href="${documentdbVsCodeLocalQuickStartDeepLink}"`)).toHaveLength(2); + + const captionStart = guided.indexOf('id="quickstart-vscode-setup-caption"'); + const caption = guided.slice(captionStart, guided.indexOf('

', captionStart)); + expect(caption).toContain('Requires'); + expect(caption).toContain('href="https://code.visualstudio.com/"'); + expect(caption).toContain('You may be prompted to install'); + expect(caption).toContain(`href="${documentdbVsCodeExtensionMarketplaceUrl}"`); + expect(guided).not.toContain('>Install the extension { + const command = panel('command'); + const guided = panel('guided'); + const editCredentials = 'Replace the username and password, then run the command.'; + const connect = 'Then connect with your preferred client.'; + expect(command).not.toContain('')); + expect(command.indexOf('Docker setup guide')).toBeGreaterThan(command.indexOf(connect)); + expect(command).not.toContain('Connect on 127.0.0.1:10260 with your app'); + expect(command).not.toContain('Run your first query.'); + expect(guided).not.toContain(' { + expect(card).not.toMatch(/version \d+\.\d+\.\d+/i); + expect(card).not.toMatch(/\d+\.\d+\.\d+ or (later|newer|above)/i); + expect(card).not.toMatch(/\bv\d+\.\d+\.\d+\b/); + }); + + it('exports a permanent full-guide link for each path without timed retry UI', () => { + expect(panel('command')).toContain('href="/docs/getting-started/docker"'); + expect(panel('command')).toContain('Docker setup guide'); + expect(panel('guided')).toContain(`href="${vscodeGuideUrl}"`); + expect(panel('guided')).toContain('VS Code setup guide'); + expect(panel('guided').indexOf('>VS Code setup guide')).toBeGreaterThan( + panel('guided').indexOf('When setup finishes, select Open Connection'), + ); + expect(card).not.toContain('Nothing happened?'); + expect(card).not.toContain('Not working in VS Code?'); + expect(panel('guided')).not.toContain('role="status"'); + }); + + it('omits the existing-instance footer from the homepage', () => { + expect(card).not.toContain('id="quickstart-existing-connection"'); + expect(card).not.toContain('Already running DocumentDB?'); + expect(card).not.toContain('Connect your existing instance in VS Code.'); + expect(card).not.toContain(`href="${existingConnectionUrl}"`); + }); +}); + +describe('VS Code quick-start guide', () => { + it('renders the existing-instance destination using the shared heading algorithm', () => { + expect(vscodeExistingConnectionSectionAnchor).toBe( + headingAnchor(vscodeExistingConnectionSectionTitle), + ); + expect(vscodeExistingConnectionSectionAnchor).toBe('connect-an-existing-instance'); + expect(guideHtml).toContain(`id="${vscodeExistingConnectionSectionAnchor}"`); + expect(guideHtml).toContain('id="set-up-document-db-local"'); + }); + + it('keeps guided setup first and connects an existing instance without provisioning', () => { + const setup = guideContent.indexOf('## Set up DocumentDB Local'); + const manual = guideContent.indexOf('## Alternative: start the container yourself'); + const connect = guideContent.indexOf(`## ${vscodeExistingConnectionSectionTitle}`); + expect(setup).toBeGreaterThan(-1); + expect(manual).toBeGreaterThan(setup); + expect(connect).toBeGreaterThan(manual); + const instructions = guideContent.slice( + connect, + guideContent.indexOf('## Verify the connection in the extension'), + ); + expect(instructions).toContain('instance that is already running'); + expect(instructions).toContain('do not need to run the setup wizard or create another container'); + expect(instructions).toContain('New Local Connection'); + expect(instructions).toContain("your instance's port"); + expect(instructions).not.toContain('docker run'); + }); + + it('explains both sample-data defaults without assuming a specific database exists', () => { + const verification = guideContent.slice( + guideContent.indexOf('## Verify the connection in the extension'), + guideContent.indexOf('## Import, export, and querying'), + ); + expect(verification).toContain('Guided setup loads sample data by default unless you turn that option off'); + expect(verification).toContain('manual Docker command above starts without sample data'); + expect(verification).toContain('An empty instance is expected when sample data is disabled'); + expect(verification).toContain('add a test document'); + expect(verification).not.toContain('StoreData'); + expect(verification).not.toContain('sampledb'); + }); + + it('retains prerequisites and launch recovery in the guide', () => { + expect(guideContent).toContain('## Prerequisites'); + expect(guideContent).toContain('Docker Desktop or Docker Engine'); + expect(guideContent).toContain('container and persistent data volume'); + expect(guideContent).toContain('It does not install Docker.'); + expect(guideContent).not.toContain('changes nothing else on your machine'); + expect(guideContent).toContain('DocumentDB: Set up DocumentDB Local'); + expect(guideContent).toContain('No extension gallery service configured'); + expect(guideContent).toContain('code --install-extension ms-azuretools.vscode-documentdb'); + }); +}); diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..21215b0 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + oxc: { + jsx: { + runtime: 'automatic', + }, + }, +});