From e93e78be0ae59c27ad032154b62eeba1496dd350 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Tue, 16 Sep 2025 15:08:28 -0500 Subject: [PATCH] feat(influxdb): Version detector shortcode triggers a modal Creates an interactive InfluxDB version detector component in TypeScript and a shortcode that generates a button to trigger the version detector modal. The shortcode takes a parameter that displays a predefined set of links for results. - Support URL pattern matching and ping header analysis - Add questionnaire-based product identification logic - Adds the shortcode in a note in /influxdb3/core/visualize-data/grafana/ - Set up TypeScript configuration for the project - Configure automatic TypeScript compilation in pre-commit hooks - Add to Grafana documentation pages - Remove last remnants of old Cypress link checker - Add Cypress tests, but many are still broken Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Apply suggestions from code review Co-authored-by: Scott Anderson Update layouts/shortcodes/influxdb-version-detector.html Co-authored-by: Scott Anderson Update assets/js/influxdb-version-detector.ts Co-authored-by: Scott Anderson Update assets/styles/components/_influxdb-version-detector.scss Co-authored-by: Scott Anderson Fixes: - Fix Hugo template to include product names in detector config - Change elimination scores from -100 to -1000 for proper filtering - Add scoring logic for generic "InfluxDB" product (OSS v2.x) - Exclude generic "InfluxDB" from results (too vague) - Add comprehensive test scenario checklist to Cypress tests - Free license now correctly excludes Enterprise, Clustered, Dedicated - Self-hosted now correctly excludes all Cloud products - SQL language now correctly excludes v1 and v2 products - Results now show only specific products (OSS 1.x, OSS 2.x, etc.) Changes: - When users answer "I'm not sure" to all questions, show a helpful message directing them to the reference table instead of showing a weak ranking with low confidence. - Detect when all questionnaire answers are "unknown" - Display custom message explaining lack of information - Auto-expand reference table for easy product identification - Hide ranked results when insufficient information provided - Make product names clickable in the quick reference table to allow users to quickly navigate to product documentation after identifying their InfluxDB version. --- .github/agents/typescript-hugo-agent.md | 545 ++++ .gitignore | 3 + TESTING.md | 2 +- assets/js/influxdb-version-detector.ts | 2407 +++++++++++++++++ assets/js/main.js | 7 +- .../_influxdb-version-detector.scss | 647 +++++ assets/styles/layouts/_modals.scss | 25 +- assets/styles/styles-default.scss | 3 + content/shared/influxdb3-visualize/grafana.md | 6 +- content/test-version-detector.md | 18 + .../content/influxdb-version-detector.cy.js | 861 ++++++ cypress/support/commands.js | 5 +- .../influxdb-version-detector-commands.js | 299 ++ cypress/support/run-e2e-specs.js | 7 +- data/products.yml | 102 + eslint.config.js | 14 +- layouts/partials/footer/modals.html | 1 + .../modals/influxdb-version-detector.html | 28 + .../shortcodes/influxdb-version-detector.html | 61 + lefthook.yml | 6 +- package.json | 4 +- tsconfig.json | 34 + yarn.lock | 6 +- 23 files changed, 5076 insertions(+), 15 deletions(-) create mode 100644 .github/agents/typescript-hugo-agent.md create mode 100644 assets/js/influxdb-version-detector.ts create mode 100644 assets/styles/components/_influxdb-version-detector.scss create mode 100644 content/test-version-detector.md create mode 100644 cypress/e2e/content/influxdb-version-detector.cy.js create mode 100644 cypress/support/influxdb-version-detector-commands.js create mode 100644 layouts/partials/footer/modals/influxdb-version-detector.html create mode 100644 layouts/shortcodes/influxdb-version-detector.html create mode 100644 tsconfig.json diff --git a/.github/agents/typescript-hugo-agent.md b/.github/agents/typescript-hugo-agent.md new file mode 100644 index 000000000..44be6d621 --- /dev/null +++ b/.github/agents/typescript-hugo-agent.md @@ -0,0 +1,545 @@ +# TypeScript & Hugo Development Agent + +You are a specialized TypeScript and Hugo development expert for the InfluxData documentation site. Your expertise spans TypeScript migration strategies, Hugo's asset pipeline, component-based architectures, and static site optimization. + +## Core Expertise + +### TypeScript Development +- **Migration Strategy**: Guide incremental TypeScript adoption in existing ES6 modules +- **Type Systems**: Create robust type definitions for documentation components +- **Configuration**: Set up optimal `tsconfig.json` for Hugo browser environments +- **Integration**: Configure TypeScript compilation within Hugo's asset pipeline +- **Compatibility**: Ensure backward compatibility during migration phases + +### Hugo Static Site Generator +- **Asset Pipeline**: Deep understanding of Hugo's extended asset processing +- **Build Process**: Optimize TypeScript compilation for Hugo's build system +- **Shortcodes**: Integrate TypeScript components with Hugo shortcodes +- **Templates**: Handle Hugo template data in TypeScript components +- **Performance**: Optimize for both development (`hugo server`) and production builds + +### Component Architecture +- **Registry Pattern**: Maintain and enhance the existing component registry system +- **Data Attributes**: Preserve `data-component` initialization pattern +- **Module System**: Work with ES6 modules and TypeScript module resolution +- **Service Layer**: Type and enhance services for API interactions +- **Utilities**: Create strongly-typed utility functions + +## Primary Responsibilities + +### 1. TypeScript Migration Setup + +#### Initial Configuration +```typescript +// tsconfig.json configuration for Hugo +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "moduleResolution": "node", + "baseUrl": "./assets/js", + "paths": { + "@components/*": ["components/*"], + "@services/*": ["services/*"], + "@utils/*": ["utils/*"] + }, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + "incremental": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "outDir": "./assets/js/dist", + "rootDir": "./assets/js" + }, + "include": ["assets/js/**/*"], + "exclude": ["node_modules", "public", "resources"] +} +``` + +#### Hugo Pipeline Integration +```yaml +# config/_default/config.yaml +build: + useResourceCacheWhen: fallback + writeStats: true + +params: + assets: + typescript: + enabled: true + sourceMap: true + minify: production +``` + +### 2. Component Migration Pattern + +#### TypeScript Component Template +```typescript +// components/example-component/example-component.ts +interface ExampleComponentConfig { + apiEndpoint?: string; + refreshInterval?: number; + onUpdate?: (data: unknown) => void; +} + +interface ExampleComponentElements { + root: HTMLElement; + trigger?: HTMLButtonElement; + content?: HTMLDivElement; +} + +export class ExampleComponent { + private config: Required; + private elements: ExampleComponentElements; + private state: Map = new Map(); + + constructor(element: HTMLElement, config: ExampleComponentConfig = {}) { + this.elements = { root: element }; + this.config = this.mergeConfig(config); + this.init(); + } + + private mergeConfig(config: ExampleComponentConfig): Required { + return { + apiEndpoint: config.apiEndpoint ?? '/api/default', + refreshInterval: config.refreshInterval ?? 5000, + onUpdate: config.onUpdate ?? (() => {}) + }; + } + + private init(): void { + this.cacheElements(); + this.bindEvents(); + this.render(); + } + + private cacheElements(): void { + this.elements.trigger = this.elements.root.querySelector('[data-trigger]') ?? undefined; + this.elements.content = this.elements.root.querySelector('[data-content]') ?? undefined; + } + + private bindEvents(): void { + this.elements.trigger?.addEventListener('click', this.handleClick.bind(this)); + } + + private handleClick(event: MouseEvent): void { + event.preventDefault(); + this.updateContent(); + } + + private async updateContent(): Promise { + // Implementation + } + + private render(): void { + // Implementation + } + + public destroy(): void { + this.elements.trigger?.removeEventListener('click', this.handleClick.bind(this)); + this.state.clear(); + } +} + +// Register with component registry +import { registerComponent } from '@utils/component-registry'; +registerComponent('example-component', ExampleComponent); +``` + +#### Migration Strategy for Existing Components +```typescript +// Step 1: Add type definitions alongside existing JS +// types/components.d.ts +declare module '@components/url-select' { + export class UrlSelect { + constructor(element: HTMLElement); + destroy(): void; + } +} + +// Step 2: Create wrapper with types +// components/url-select/url-select.ts +import { UrlSelect as UrlSelectJS } from './url-select.js'; + +export interface UrlSelectConfig { + storageKey?: string; + defaultUrl?: string; +} + +export class UrlSelect extends UrlSelectJS { + constructor(element: HTMLElement, config?: UrlSelectConfig) { + super(element, config); + } +} + +// Step 3: Gradually migrate internals to TypeScript +``` + +### 3. Hugo Asset Pipeline Configuration + +#### TypeScript Processing with Hugo Pipes +```javascript +// assets/js/main.ts (entry point) +import { ComponentRegistry } from './utils/component-registry'; +import './components/index'; // Auto-register all components + +// Initialize on DOM ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', ComponentRegistry.initializeAll); +} else { + ComponentRegistry.initializeAll(); +} + +// Hugo template integration +{{ $opts := dict "targetPath" "js/main.js" "minify" (eq hugo.Environment "production") }} +{{ $ts := resources.Get "js/main.ts" | js.Build $opts }} +{{ if eq hugo.Environment "development" }} + +{{ else }} + {{ $ts = $ts | fingerprint }} + +{{ end }} +``` + +#### Build Performance Optimization +```typescript +// utils/lazy-loader.ts +export class LazyLoader { + private static cache = new Map>(); + + static async loadComponent(name: string): Promise { + if (!this.cache.has(name)) { + this.cache.set(name, + import(/* webpackChunkName: "[request]" */ `@components/${name}/${name}`) + ); + } + return this.cache.get(name); + } +} + +// Usage in component registry +async function initializeComponent(element: HTMLElement): Promise { + const componentName = element.dataset.component; + if (!componentName) return; + + const module = await LazyLoader.loadComponent(componentName); + const Component = module.default || module[componentName]; + new Component(element); +} +``` + +### 4. Type Definitions for Hugo Context + +```typescript +// types/hugo.d.ts +interface HugoPage { + title: string; + description: string; + permalink: string; + section: string; + params: Record; +} + +interface HugoSite { + baseURL: string; + languageCode: string; + params: { + influxdb_urls: Array<{ + url: string; + name: string; + cloud?: boolean; + }>; + }; +} + +declare global { + interface Window { + Hugo: { + page: HugoPage; + site: HugoSite; + }; + docsData: { + page: HugoPage; + site: HugoSite; + }; + } +} + +export {}; +``` + +### 5. Testing Integration + +#### Cypress with TypeScript +```typescript +// cypress/support/commands.ts +Cypress.Commands.add('initComponent', (componentName: string, config?: Record) => { + cy.window().then((win) => { + const element = win.document.querySelector(`[data-component="${componentName}"]`); + if (element) { + // Initialize component with TypeScript + const event = new CustomEvent('component:init', { detail: config }); + element.dispatchEvent(event); + } + }); +}); + +// cypress/support/index.d.ts +declare namespace Cypress { + interface Chainable { + initComponent(componentName: string, config?: Record): Chainable; + } +} +``` + +### 6. Development Workflow + +#### NPM Scripts for TypeScript Development +```json +{ + "scripts": { + "ts:check": "tsc --noEmit", + "ts:build": "tsc", + "ts:watch": "tsc --watch", + "dev": "concurrently \"yarn ts:watch\" \"hugo server\"", + "build": "yarn ts:build && hugo --minify", + "lint:ts": "eslint 'assets/js/**/*.ts'", + "format:ts": "prettier --write 'assets/js/**/*.ts'" + } +} +``` + +#### VSCode Configuration +```json +// .vscode/settings.json +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } + }, + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.quoteStyle": "single" +} +``` + +## Migration Guidelines + +### Phase 1: Setup (Week 1) +1. Install TypeScript and type definitions +2. Configure `tsconfig.json` for Hugo environment +3. Set up build scripts and Hugo pipeline +4. Create type definitions for existing globals + +### Phase 2: Type Definitions (Week 2) +1. Create `.d.ts` files for existing JS modules +2. Add type definitions for Hugo context +3. Type external dependencies +4. Set up ambient declarations + +### Phase 3: Incremental Migration (Weeks 3-8) +1. Start with utility modules (pure functions) +2. Migrate service layer (API interactions) +3. Convert leaf components (no dependencies) +4. Migrate complex components +5. Update component registry + +### Phase 4: Optimization (Week 9-10) +1. Implement code splitting +2. Set up lazy loading +3. Optimize build performance +4. Configure production builds + +## Best Practices + +### TypeScript Conventions +- Use strict mode from the start +- Prefer interfaces over type aliases for objects +- Use const assertions for literal types +- Implement proper error boundaries +- Use discriminated unions for state management + +### Hugo Integration +- Leverage Hugo's build stats for optimization +- Use resource bundling for related assets +- Implement proper cache busting +- Utilize Hugo's minification in production +- Keep source maps in development only + +### Component Guidelines +- One component per file +- Co-locate types with implementation +- Use composition over inheritance +- Implement cleanup in destroy methods +- Follow single responsibility principle + +### Performance Considerations +- Use dynamic imports for large components +- Implement intersection observer for lazy loading +- Minimize bundle size with tree shaking +- Use TypeScript's `const enum` for better optimization +- Leverage browser caching strategies + +## Debugging Strategies + +### Development Tools +```typescript +// utils/debug.ts +export const debug = { + log: (component: string, message: string, data?: unknown): void => { + if (process.env.NODE_ENV === 'development') { + console.log(`[${component}]`, message, data); + } + }, + + time: (label: string): void => { + if (process.env.NODE_ENV === 'development') { + console.time(label); + } + }, + + timeEnd: (label: string): void => { + if (process.env.NODE_ENV === 'development') { + console.timeEnd(label); + } + } +}; +``` + +### Source Maps Configuration +```javascript +// hugo.config.js for development +module.exports = { + module: { + rules: [{ + test: /\.ts$/, + use: { + loader: 'ts-loader', + options: { + compilerOptions: { + sourceMap: true + } + } + } + }] + }, + devtool: 'inline-source-map' +}; +``` + +## Common Patterns + +### State Management +```typescript +// utils/state-manager.ts +export class StateManager> { + private state: T; + private listeners: Set<(state: T) => void> = new Set(); + + constructor(initialState: T) { + this.state = { ...initialState }; + } + + get current(): Readonly { + return Object.freeze({ ...this.state }); + } + + update(updates: Partial): void { + this.state = { ...this.state, ...updates }; + this.notify(); + } + + subscribe(listener: (state: T) => void): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + private notify(): void { + this.listeners.forEach(listener => listener(this.current)); + } +} +``` + +### API Service Pattern +```typescript +// services/base-service.ts +export abstract class BaseService { + protected baseURL: string; + protected headers: HeadersInit; + + constructor(baseURL: string = '') { + this.baseURL = baseURL; + this.headers = { + 'Content-Type': 'application/json' + }; + } + + protected async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseURL}${endpoint}`; + const config: RequestInit = { + ...options, + headers: { ...this.headers, ...options.headers } + }; + + const response = await fetch(url, config); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + } +} +``` + +## Troubleshooting Guide + +### Common Issues and Solutions + +1. **Module Resolution Issues** + - Check `tsconfig.json` paths configuration + - Verify Hugo's asset directory structure + - Ensure proper file extensions in imports + +2. **Type Definition Conflicts** + - Use namespace isolation for global types + - Check for duplicate declarations + - Verify ambient module declarations + +3. **Build Performance** + - Enable incremental compilation + - Use project references for large codebases + - Implement proper code splitting + +4. **Runtime Errors** + - Verify TypeScript target matches browser support + - Check for proper polyfills + - Ensure correct module format for Hugo + +5. **Hugo Integration Issues** + - Verify resource pipeline configuration + - Check for proper asset fingerprinting + - Ensure correct build environment detection + +## Reference Documentation + +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [Hugo Pipes Documentation](https://gohugo.io/hugo-pipes/) +- [ESBuild with Hugo](https://gohugo.io/hugo-pipes/js/) +- [TypeScript ESLint](https://typescript-eslint.io/) +- [Cypress TypeScript](https://docs.cypress.io/guides/tooling/typescript-support) \ No newline at end of file diff --git a/.gitignore b/.gitignore index 32765da72..4e0b03b35 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,9 @@ tmp .idea **/config.toml +# TypeScript build output +**/dist/ + # User context files for AI assistant tools .context/* !.context/README.md diff --git a/TESTING.md b/TESTING.md index 233bb3a36..b012eb664 100644 --- a/TESTING.md +++ b/TESTING.md @@ -431,7 +431,7 @@ LEFTHOOK=0 git commit yarn test:e2e # Run specific E2E specs -node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/article-links.cy.js" +node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/index.cy.js" ``` ### JavaScript Testing and Debugging diff --git a/assets/js/influxdb-version-detector.ts b/assets/js/influxdb-version-detector.ts new file mode 100644 index 000000000..9f94babb5 --- /dev/null +++ b/assets/js/influxdb-version-detector.ts @@ -0,0 +1,2407 @@ +/** + * InfluxDB Version Detector Component + * + * Helps users identify which InfluxDB product they're using through a + * guided questionnaire with URL detection and scoring-based recommendations. + * + * DECISION TREE LOGIC (from .context/drafts/influxdb-version-detector/influxdb-decision-tree.md): + * + * ## Primary Detection Flow + * + * START: User enters URL + * | + * ├─→ URL matches known cloud patterns? + * │ │ + * │ ├─→ YES: Contains "influxdb.io" → **InfluxDB Cloud Dedicated** ✓ + * │ ├─→ YES: Contains "cloud2.influxdata.com" regions → **InfluxDB Cloud Serverless** ✓ + * │ ├─→ YES: Contains "influxcloud.net" → **InfluxDB Cloud 1** ✓ + * │ └─→ YES: Contains other cloud2 regions → **InfluxDB Cloud (TSM)** ✓ + * │ + * └─→ NO: Check port and try /ping endpoint + * │ + * ├─→ Port 8181 detected? → Strong indicator of v3 (Core/Enterprise) + * | | Returns 200 (auth successful or disabled)? + * | │ │--> `x-influxdb-build: Enterprise` -> **InfluxDB 3 Enterprise** ✓ (definitive) + * | │ │--> `x-influxdb-build: Core` -> **InfluxDB 3 Core** ✓ (definitive) + * │ │ + * │ ├─→ Returns 401 Unauthorized (default - auth required)? + * │ │ + * │ └─→ Ask "Paid or Free?" + * │ ├─→ Paid → **InfluxDB 3 Enterprise** ✓ (definitive) + * │ └─→ Free → **InfluxDB 3 Core** ✓ (definitive) + * | + * ├─→ Port 8086 detected? → Strong indicator of legacy (OSS/Enterprise) + * │ │ ⚠️ NOTE: v1.x ping auth optional (ping-auth-enabled), v2.x always open + * │ │ + * │ ├─→ Returns 401 Unauthorized? + * │ │ │ Could be v1.x with ping-auth-enabled=true OR Enterprise + * │ │ │ + * │ │ └─→ Ask "Paid or Free?" → Show ranked results + * │ │ + * │ ├─→ Returns 200/204 (accessible)? + * │ │ │ Likely v2.x OSS (always open) or v1.x with ping-auth-enabled=false + * │ │ │ + * │ │ └─→ Continue to questionnaire + * │ + * └─→ Blocked/Can't detect? + * │ + * └─→ Start questionnaire + * + * ## Questionnaire Flow (No URL or after detection) + * + * Q1: Which type of license do you have? + * ├─→ Paid/Commercial License + * ├─→ Free/Open Source (including free cloud tiers) + * └─→ I'm not sure + * + * Q2: Is your InfluxDB hosted by InfluxData (cloud) or self-hosted? + * ├─→ Cloud service (hosted by InfluxData) + * ├─→ Self-hosted (on your own servers) + * └─→ I'm not sure + * + * Q3: How long has your server been in place? + * ├─→ Recently installed (less than 1 year) + * ├─→ 1-5 years + * ├─→ More than 5 years + * └─→ I'm not sure + * + * Q4: Which query language(s) do you use? + * ├─→ SQL + * ├─→ InfluxQL + * ├─→ Flux + * ├─→ Multiple languages + * └─→ I'm not sure + * + * ## Definitive Determinations (Stop immediately, no more questions) + * + * 1. **401 + Port 8181 + Paid** → InfluxDB 3 Enterprise ✓ + * 2. **401 + Port 8181 + Free** → InfluxDB 3 Core ✓ + * 3. **URL matches cloud pattern** → Specific cloud product ✓ + * 4. **x-influxdb-build header** → Definitive product identification ✓ + * + * ## Scoring System (When not definitive) + * + * ### Elimination Rules + * - **Free + Self-hosted** → Eliminates all cloud products + * - **Free** → Eliminates: 3 Enterprise, Enterprise, Clustered, Cloud Dedicated, Cloud 1 + * - **Paid + Self-hosted** → Eliminates all cloud products + * - **Paid + Cloud** → Eliminates all self-hosted products + * - **Free + Cloud** → Eliminates all self-hosted products, favors Serverless/TSM + * + * ### Strong Signals (High points) + * - **401 Response**: +50 for v3 products, +30 for Clustered + * - **Port 8181**: +30 for v3 products + * - **Port 8086**: +20 for legacy products + * - **SQL Language**: +40 for v3 products, eliminates v1/v2 + * - **Flux Language**: +30 for v2 era, eliminates v1 and v3 + * - **Server Age 5+ years**: +30 for v1 products, -50 for v3 + * + * ### Ranking Display Rules + * - Only show "Most Likely" if: + * - Top score > 30 (not low confidence) + * - AND difference between #1 and #2 is ≥ 15 points + * - Show manual verification commands only if: + * - Confidence is not high (score < 60) + * - AND it's a self-hosted product + * - AND user didn't say it's cloud + */ + +import { getInfluxDBUrls } from './services/local-storage.js'; + +interface QueryLanguageConfig { + required_params: string[]; + optional_params?: string[]; +} + +interface ProductConfig { + name?: string; + query_languages: Record; + characteristics: string[]; + placeholder_host?: string; + detection?: { + url_contains?: string[]; + ping_headers?: Record; + }; +} + +interface Products { + [key: string]: ProductConfig; +} + +interface Answers { + context?: string | null; + portClue?: string | null; + isCloud?: boolean; + isDocker?: boolean; + paid?: string; + hosted?: string; + age?: string; + language?: string; + auth?: string; + data?: string; + version?: string; + [key: string]: string | boolean | null | undefined; +} + +interface ComponentOptions { + component: HTMLElement; +} + +interface AnalyticsEventData { + detected_product?: string; + detection_method?: string; + interaction_type: string; + section?: string; + completion_status?: string; + question_id?: string; + answer_value?: string; +} + +// Global gtag function type declaration +declare global { + interface Window { + gtag?: ( + _event: string, + _action: string, + _parameters?: Record + ) => void; + } +} + +class InfluxDBVersionDetector { + private container: HTMLElement; + private products: Products; + private influxdbUrls: Record; + private answers: Answers = {}; + private initialized: boolean = false; + private questionFlow: string[] = []; + private currentQuestionIndex = 0; + private questionHistory: string[] = []; // Track question history for back navigation + private progressBar: HTMLElement | null = null; + private resultDiv: HTMLElement | null = null; + private restartBtn: HTMLElement | null = null; + private currentContext: 'questionnaire' | 'result' = 'questionnaire'; + + constructor(options: ComponentOptions) { + this.container = options.component; + + // Parse data attributes from the component element + const { products, influxdbUrls } = this.parseComponentData(); + + this.products = products; + this.influxdbUrls = influxdbUrls; + + // Check if component is in a modal + const modal = this.container.closest('.modal-content'); + if (modal) { + // If in modal, wait for modal to be opened before initializing + this.initializeForModal(); + } else { + // If not in modal, initialize immediately + this.init(); + } + } + + private parseComponentData(): { + products: Products; + influxdbUrls: Record; + } { + let products: Products = {}; + let influxdbUrls: Record = {}; + + // Parse products data - Hugo always provides this data + const productsData = this.container.getAttribute('data-products'); + if (productsData) { + try { + products = JSON.parse(productsData); + } catch (error) { + console.warn('Failed to parse products data:', error); + } + } + + // Parse influxdb URLs data + const influxdbUrlsData = this.container.getAttribute('data-influxdb-urls'); + if (influxdbUrlsData && influxdbUrlsData !== '#ZgotmplZ') { + try { + influxdbUrls = JSON.parse(influxdbUrlsData); + } catch (error) { + console.warn('Failed to parse influxdb_urls data:', error); + influxdbUrls = {}; // Fallback to empty object + } + } else { + console.debug( + 'InfluxDB URLs data not available or blocked by template security. ' + + 'This is expected when Hugo data is unavailable.' + ); + influxdbUrls = {}; // Fallback to empty object + } + + return { products, influxdbUrls }; + } + + private init(): void { + this.render(); + this.setupPlaceholders(); + this.attachEventListeners(); + this.showQuestion('q-url-known'); + this.initialized = true; + + // Track modal opening + this.trackAnalyticsEvent({ + interaction_type: 'modal_opened', + section: this.getCurrentPageSection(), + }); + } + + private setupPlaceholders(): void { + // This method is called at init but some placeholders need to be set + // when questions are actually displayed since DOM elements don't exist yet + } + + private setupPingHeadersPlaceholder(): void { + const pingHeaders = this.container.querySelector('#ping-headers'); + if (pingHeaders) { + const exampleContent = [ + '# Replace this with your actual response headers', + '# Example formats:', + '', + '# InfluxDB 3 Core:', + 'HTTP/1.1 200 OK', + 'x-influxdb-build: core', + 'x-influxdb-version: 3.1.0', + '', + '# InfluxDB 3 Enterprise:', + 'HTTP/1.1 200 OK', + 'x-influxdb-build: enterprise', + 'x-influxdb-version: 3.1.0', + '', + '# InfluxDB v2 OSS:', + 'HTTP/1.1 204 No Content', + 'X-Influxdb-Build: OSS', + 'X-Influxdb-Version: 2.7.8', + '', + '# InfluxDB v1:', + 'HTTP/1.1 204 No Content', + 'X-Influxdb-Version: 1.8.10', + ].join('\n'); + + (pingHeaders as HTMLTextAreaElement).value = exampleContent; + + // Select all text when user clicks in the textarea so they can easily replace it + pingHeaders.addEventListener('focus', () => { + (pingHeaders as HTMLTextAreaElement).select(); + }); + } + } + + private setupDockerOutputPlaceholder(): void { + const dockerOutput = this.container.querySelector('#docker-output'); + if (dockerOutput) { + const exampleContent = [ + '# Replace this with your actual command output', + '# Example formats:', + '', + '# Version command output:', + 'InfluxDB 3.1.0 (git: abc123def)', + 'or', + 'InfluxDB v2.7.8 (git: 407fa622e)', + '', + '# Ping headers from curl -I:', + 'HTTP/1.1 200 OK', + 'x-influxdb-build: core', + 'x-influxdb-version: 3.1.0', + '', + '# Startup logs:', + '2024-01-01T00:00:00.000Z info InfluxDB starting', + '2024-01-01T00:00:00.000Z info InfluxDB 3.1.0 (git: abc123)', + ].join('\n'); + + (dockerOutput as HTMLTextAreaElement).value = exampleContent; + + // Select all text when user clicks in the textarea so they can easily replace it + dockerOutput.addEventListener('focus', () => { + (dockerOutput as HTMLTextAreaElement).select(); + }); + } + } + + private getCurrentPageSection(): string { + // Extract meaningful section from current page + const path = window.location.pathname; + const pathSegments = path.split('/').filter((segment) => segment); + + // Try to get a meaningful section name + if (pathSegments.length >= 3) { + return pathSegments.slice(0, 3).join('/'); // e.g., "influxdb3/core/visualize-data" + } else if (pathSegments.length >= 2) { + return pathSegments.slice(0, 2).join('/'); // e.g., "influxdb3/core" + } + + return path || 'unknown'; + } + + private trackAnalyticsEvent(eventData: AnalyticsEventData): void { + // Track Google Analytics events following the pattern from code-controls.js + try { + // Get current page context + const currentUrl = new URL(window.location.href); + const path = window.location.pathname; + + // Determine product context from current page + let pageContext = 'other'; + if (/\/influxdb\/cloud\//.test(path)) { + pageContext = 'cloud'; + } else if (/\/influxdb3\/core/.test(path)) { + pageContext = 'core'; + } else if (/\/influxdb3\/enterprise/.test(path)) { + pageContext = 'enterprise'; + } else if (/\/influxdb3\/cloud-serverless/.test(path)) { + pageContext = 'serverless'; + } else if (/\/influxdb3\/cloud-dedicated/.test(path)) { + pageContext = 'dedicated'; + } else if (/\/influxdb3\/clustered/.test(path)) { + pageContext = 'clustered'; + } else if (/\/(enterprise_|influxdb).*\/v[1-2]\//.test(path)) { + pageContext = 'oss/enterprise'; + } + + // Add tracking parameters to URL (following code-controls.js pattern) + if (eventData.detected_product) { + switch (eventData.detected_product) { + case 'core': + currentUrl.searchParams.set('dl', 'oss3'); + break; + case 'enterprise': + currentUrl.searchParams.set('dl', 'enterprise'); + break; + case 'cloud': + case 'cloud-v1': + case 'cloud-v2-tsm': + currentUrl.searchParams.set('dl', 'cloud'); + break; + case 'serverless': + currentUrl.searchParams.set('dl', 'serverless'); + break; + case 'dedicated': + currentUrl.searchParams.set('dl', 'dedicated'); + break; + case 'clustered': + currentUrl.searchParams.set('dl', 'clustered'); + break; + case 'oss': + case 'oss-v1': + case 'oss-v2': + currentUrl.searchParams.set('dl', 'oss'); + break; + } + } + + // Add additional tracking parameters + if (eventData.detection_method) { + currentUrl.searchParams.set( + 'detection_method', + eventData.detection_method + ); + } + if (eventData.completion_status) { + currentUrl.searchParams.set('completion', eventData.completion_status); + } + if (eventData.section) { + currentUrl.searchParams.set( + 'section', + encodeURIComponent(eventData.section) + ); + } + + // Update browser history without triggering page reload + if (window.history && window.history.replaceState) { + window.history.replaceState(null, '', currentUrl.toString()); + } + + // Send custom Google Analytics event if gtag is available + if (typeof window.gtag !== 'undefined') { + window.gtag('event', 'influxdb_version_detector', { + interaction_type: eventData.interaction_type, + detected_product: eventData.detected_product, + detection_method: eventData.detection_method, + completion_status: eventData.completion_status, + question_id: eventData.question_id, + answer_value: eventData.answer_value, + section: eventData.section, + page_context: pageContext, + custom_map: { + dimension1: eventData.detected_product, + dimension2: eventData.detection_method, + dimension3: pageContext, + }, + }); + } + } catch (error) { + // Silently handle analytics errors to avoid breaking functionality + console.debug('Analytics tracking error:', error); + } + } + + private initializeForModal(): void { + // Set up event listener to initialize when modal opens + const modalContent = this.container.closest('.modal-content'); + if (!modalContent) return; + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if ( + mutation.type === 'attributes' && + mutation.attributeName === 'style' + ) { + const target = mutation.target as HTMLElement; + const isVisible = + target.style.display !== 'none' && target.style.display !== ''; + + if (isVisible && !this.initialized) { + // Modal just opened and component not yet initialized + this.init(); + observer.disconnect(); + } + } + }); + }); + + // Start observing the modal content for style changes + observer.observe(modalContent, { + attributes: true, + attributeFilter: ['style'], + }); + + // Also check if modal is already visible + const computedStyle = window.getComputedStyle(modalContent); + if (computedStyle.display !== 'none' && !this.initialized) { + this.init(); + observer.disconnect(); + } + } + + private getBasicUrlSuggestion(): string { + // Provide a basic placeholder URL suggestion based on common patterns + return 'https://your-influxdb-host.com:8086'; + } + + private getProductDisplayName(product: string): string { + const displayNames: Record = { + // Simplified product keys (used in detection results) + 'oss-v1': 'InfluxDB OSS v1.x', + 'oss-v2': 'InfluxDB OSS v2.x', + oss: 'InfluxDB OSS (version unknown)', + cloud: 'InfluxDB Cloud', + 'cloud-v1': 'InfluxDB Cloud v1', + 'cloud-v2-tsm': 'InfluxDB Cloud v2 (TSM)', + serverless: 'InfluxDB Cloud Serverless', + core: 'InfluxDB 3 Core', + enterprise: 'InfluxDB 3 Enterprise', + dedicated: 'InfluxDB Cloud Dedicated', + clustered: 'InfluxDB Clustered', + custom: 'Custom URL', + + // Raw product keys from products.yml (used in scoring) + influxdb3_core: 'InfluxDB 3 Core', + influxdb3_enterprise: 'InfluxDB 3 Enterprise', + influxdb3_cloud_serverless: 'InfluxDB Cloud Serverless', + influxdb3_cloud_dedicated: 'InfluxDB Cloud Dedicated', + influxdb3_clustered: 'InfluxDB Clustered', + influxdb_v1: 'InfluxDB OSS v1.x', + influxdb_v2: 'InfluxDB OSS v2.x', + enterprise_influxdb: 'InfluxDB Enterprise v1.x', + influxdb: 'InfluxDB OSS v2.x', + }; + displayNames['core or enterprise'] = + `${displayNames.core} or ${displayNames.enterprise}`; + return displayNames[product] || product; + } + + private generateConfigurationGuidance(productKey: string): string { + // Map from result product names to products.yml keys + const productMapping: Record = { + core: 'influxdb3_core', + enterprise: 'influxdb3_enterprise', + serverless: 'influxdb3_cloud_serverless', + dedicated: 'influxdb3_cloud_dedicated', + clustered: 'influxdb3_clustered', + 'oss-v1': 'influxdb_v1', + 'oss-v2': 'influxdb_v2', + }; + + const dataKey = productMapping[productKey]; + if (!dataKey || !this.products[dataKey]) { + return ''; + } + + const productConfig = this.products[dataKey]; + const productName = this.getProductDisplayName(productKey); + + if ( + !productConfig.query_languages || + Object.keys(productConfig.query_languages).length === 0 + ) { + return ''; + } + + let html = ` +
+

Configuration Parameter Meanings for ${productName}

+

When configuring Grafana or other tools to connect to your ${productName} instance, these parameters mean:

+ `; + + // Add HOST explanation + const hostExample = this.getHostExample(dataKey); + html += ` +
+ HOST/URL: The network address where your ${productName} instance is running
+ + For your setup, this would typically be: ${hostExample} + +
+ `; + + // Add database/bucket terminology explanation + const usesDatabase = this.usesDatabaseTerminology(productConfig); + if (usesDatabase) { + html += ` +
+ DATABASE: The named collection where your data is stored
+ + ${productName} uses "database" terminology for organizing your time series data + +
+ `; + } else { + html += ` +
+ BUCKET: The named collection where your data is stored
+ + ${productName} uses "bucket" terminology for organizing your time series data + +
+ `; + } + + // Add authentication explanation + const authInfo = this.getAuthenticationInfo(productConfig); + html += ` +
+ AUTHENTICATION: ${authInfo.description}
+ + ${authInfo.details} + +
+ `; + + // Add query language explanation + const languages = Object.keys(productConfig.query_languages).join(', '); + html += ` +
+ QUERY LANGUAGE: The syntax used to retrieve your data
+ + ${productName} supports: ${languages} + +
+ `; + + html += '
'; + return html; + } + + private getHostExample(productDataKey: string): string { + // Extract placeholder_host from the products data if available + const productData = this.products[productDataKey]; + + // Use placeholder_host from the product configuration if available + if (productData?.placeholder_host) { + // Add protocol if not present + const host = productData.placeholder_host; + if (host.startsWith('http://') || host.startsWith('https://')) { + return host; + } else { + // Default to http for localhost, https for others + return host.includes('localhost') + ? `http://${host}` + : `https://${host}`; + } + } + + // Fallback based on product type + const hostExamples: Record = { + influxdb3_core: 'http://localhost:8181', + influxdb3_enterprise: 'http://localhost:8181', + influxdb3_cloud_serverless: 'https://cloud2.influxdata.com', + influxdb3_cloud_dedicated: 'https://cluster-id.a.influxdb.io', + influxdb3_clustered: 'https://cluster-host.com', + influxdb_v1: 'http://localhost:8086', + influxdb_v2: 'http://localhost:8086', + }; + + return hostExamples[productDataKey] || 'http://localhost:8086'; + } + + private usesDatabaseTerminology(productConfig: ProductConfig): boolean { + // Check if any query language uses 'Database' parameter + for (const language of Object.values(productConfig.query_languages)) { + if (language.required_params.includes('Database')) { + return true; + } + } + return false; + } + + private getAuthenticationInfo(productConfig: ProductConfig): { + description: string; + details: string; + } { + // Check if any query language requires Token + const requiresToken = Object.values(productConfig.query_languages).some( + (lang) => lang.required_params.includes('Token') + ); + + // Determine if this product uses "database" or "bucket" terminology + const usesDatabaseTerm = this.usesDatabaseTerminology(productConfig); + const resourceName = usesDatabaseTerm ? 'database' : 'bucket'; + + if (requiresToken) { + return { + description: 'Token-based authentication required', + details: `You need a valid API token with appropriate permissions for your ${resourceName}`, + }; + } else { + return { + description: 'No authentication required by default', + details: + 'This instance typically runs without authentication, though it may be optionally configured', + }; + } + } + + private detectEnterpriseFeatures(): { + likelyProduct: string; + confidence: number; + } | null { + // According to the decision tree, we cannot reliably distinguish + // Core vs Enterprise from URL alone. The real differentiator is: + // - Both Enterprise and Core: /ping requires auth by default (opt-out possible) + // - Definitive identification requires x-influxdb-build header from 200 response + // + // Since this component cannot make HTTP requests to test /ping, + // we return null to indicate we cannot distinguish them from URL alone. + + return null; + } + + private analyzeUrlPatterns(url: string): { + likelyProduct: string | null; + confidence: number; + suggestion?: string; + } { + if (!url || !this.influxdbUrls) { + return { likelyProduct: null, confidence: 0 }; + } + + const urlLower = url.toLowerCase(); + + // PRIORITY 1: Check for definitive cloud patterns first (per decision tree) + // These should be checked before localhost patterns for accuracy + + // InfluxDB Cloud Dedicated: Contains "influxdb.io" + if (urlLower.includes('influxdb.io')) { + return { likelyProduct: 'dedicated', confidence: 1.0 }; + } + + // InfluxDB Cloud Serverless: Contains "cloud2.influxdata.com" regions + if (urlLower.includes('cloud2.influxdata.com')) { + // Check for specific Serverless regions + const serverlessRegions = [ + 'us-east-1-1.aws.cloud2.influxdata.com', + 'eu-central-1-1.aws.cloud2.influxdata.com', + ]; + + for (const region of serverlessRegions) { + if (urlLower.includes(region.toLowerCase())) { + return { likelyProduct: 'serverless', confidence: 1.0 }; + } + } + + // Other cloud2 regions default to InfluxDB Cloud v2 (TSM) + return { likelyProduct: 'cloud-v2-tsm', confidence: 0.9 }; + } + + // InfluxDB Cloud v1 (legacy): Contains "influxcloud.net" + if (urlLower.includes('influxcloud.net')) { + return { likelyProduct: 'cloud-v1', confidence: 1.0 }; + } + + // PRIORITY 2: Check for localhost/port-based patterns (OSS, Core, Enterprise) + // Note: localhost URLs cannot be cloud versions - they're always self-hosted + if (urlLower.includes('localhost') || urlLower.includes('127.0.0.1')) { + // OSS default port + if (urlLower.includes(':8086')) { + return { + likelyProduct: 'oss', + confidence: 0.8, + suggestion: 'version-check', + }; + } + + // Core/Enterprise default port - both use 8181 + if (urlLower.includes(':8181')) { + // Try to distinguish between Core and Enterprise + const enterpriseResult = this.detectEnterpriseFeatures(); + if (enterpriseResult) { + return enterpriseResult; + } + + // Can't distinguish from URL alone - suggest ping test + return { + likelyProduct: 'core or enterprise', + confidence: 0.7, + suggestion: 'ping-test', + }; + } + } + + // Then check cloud products with provider regions + // Skip this check if URL is localhost (cannot be cloud) + const isLocalhost = + urlLower.includes('localhost') || urlLower.includes('127.0.0.1'); + if (!isLocalhost) { + for (const [productKey, productData] of Object.entries( + this.influxdbUrls + )) { + if (!productData || typeof productData !== 'object') continue; + + const providers = (productData as Record).providers; + if (!Array.isArray(providers)) continue; + + for (const provider of providers) { + if (!provider.regions) continue; + + for (const region of provider.regions) { + if (region.url) { + const patternUrl = region.url.toLowerCase(); + + // Exact match + if (urlLower === patternUrl) { + return { likelyProduct: productKey, confidence: 1.0 }; + } + + // Domain match for cloud URLs + if ( + productKey === 'cloud' && + urlLower.includes('cloud2.influxdata.com') + ) { + return { likelyProduct: 'cloud', confidence: 0.9 }; + } + } + } + } + } + } + + // Additional heuristics based on common patterns + // Special handling for user inputs like "cloud 2", "cloud v2", etc. + // Skip cloud heuristics for localhost URLs + if (!isLocalhost) { + if (urlLower.match(/cloud\s*[v]?2/)) { + return { likelyProduct: 'cloud', confidence: 0.8 }; + } + + if ( + urlLower.includes('cloud') || + urlLower.includes('aws') || + urlLower.includes('azure') || + urlLower.includes('gcp') + ) { + return { likelyProduct: 'cloud', confidence: 0.6 }; + } + } + + // Port-based suggestions for unknown/invalid URLs + if (urlLower.includes(':8086')) { + return { + likelyProduct: 'oss-port', + confidence: 0.4, + suggestion: 'multiple-candidates-8086', + }; + } + + if (urlLower.includes(':8181')) { + return { + likelyProduct: 'v3-port', + confidence: 0.4, + suggestion: 'multiple-candidates-8181', + }; + } + + return { likelyProduct: null, confidence: 0 }; + } + + private render(): void { + this.container.innerHTML = ` +
+

+ InfluxDB product detector +

+

+ Answer a few questions to identify which InfluxDB product you're using +

+ +
+
+
+ +
+ +
+
+ Do you know the URL of your InfluxDB server? +
+ + + + +
+ + +
+
+ Please enter your InfluxDB server URL: +
+
+ +
+ + +
+ + +
+
+ For airgapped environments, run this command from a machine that can + access your InfluxDB: +
+
curl -I http://your-influxdb-url:8086/ping
+
+ Then paste the response headers here: +
+ +
+ + +
+
+ + +
+
+ For Docker/Kubernetes environments, run these commands to identify your InfluxDB version: +
+
+ First, find your container: +
+
docker ps | grep influx
+
+ Then run one of these commands (replace <container> with your container name/ID): +
+
# Get version info: +docker exec <container> influxd version + +# Get ping headers: +docker exec <container> curl -I localhost:8086/ping + +# Or check startup logs: +docker logs <container> 2>&1 | head -20
+
+ Paste the output here: +
+ +
+ + +
+
+ + +
+
+ Which type of InfluxDB license do you have? +
+ + + + +
+ + +
+
+ Is your InfluxDB instance hosted by InfluxData (cloud) or + self-hosted? +
+ + + + +
+ + +
+
How long has your InfluxDB server been in place?
+ + + + + +
+ + +
+
Which query language(s) do you use with InfluxDB?
+ + + + + + +
+
+ +
+ + +
+ `; + + // Cache DOM elements + this.progressBar = this.container.querySelector('#progress-bar'); + this.resultDiv = this.container.querySelector('#result'); + this.restartBtn = this.container.querySelector('#restart-btn'); + } + + private attachEventListeners(): void { + this.container.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + + if ( + target.classList.contains('option-button') || + target.classList.contains('submit-button') || + target.classList.contains('back-button') + ) { + const action = target.dataset.action; + + switch (action) { + case 'url-known': + this.trackAnalyticsEvent({ + interaction_type: 'question_answered', + question_id: 'url-known', + answer_value: target.dataset.value || '', + section: this.getCurrentPageSection(), + }); + this.handleUrlKnown(target.dataset.value); + break; + case 'go-back': + this.trackAnalyticsEvent({ + interaction_type: 'navigation', + section: this.getCurrentPageSection(), + }); + this.goBack(); + break; + case 'detect-url': + this.trackAnalyticsEvent({ + interaction_type: 'url_detection_attempt', + detection_method: 'url_analysis', + section: this.getCurrentPageSection(), + }); + this.detectByUrl(); + break; + case 'analyze-headers': + this.trackAnalyticsEvent({ + interaction_type: 'manual_analysis', + detection_method: 'ping_headers', + section: this.getCurrentPageSection(), + }); + this.analyzePingHeaders(); + break; + case 'analyze-docker': + this.trackAnalyticsEvent({ + interaction_type: 'manual_analysis', + detection_method: 'docker_output', + section: this.getCurrentPageSection(), + }); + this.analyzeDockerOutput(); + break; + case 'answer': + this.trackAnalyticsEvent({ + interaction_type: 'question_answered', + question_id: target.dataset.category || '', + answer_value: target.dataset.value || '', + section: this.getCurrentPageSection(), + }); + this.answerQuestion( + target.dataset.category!, + target.dataset.value! + ); + break; + case 'auth-help-answer': + this.trackAnalyticsEvent({ + interaction_type: 'auth_help_response', + question_id: target.dataset.category || '', + answer_value: target.dataset.value || '', + section: this.getCurrentPageSection(), + }); + this.handleAuthorizationHelp( + target.dataset.category!, + target.dataset.value! + ); + break; + case 'restart': + this.trackAnalyticsEvent({ + interaction_type: 'restart', + section: this.getCurrentPageSection(), + }); + this.restart(); + break; + case 'start-questionnaire': { + this.trackAnalyticsEvent({ + interaction_type: 'start_questionnaire', + section: this.getCurrentPageSection(), + }); + // Hide result and restart button first + if (this.resultDiv) { + this.resultDiv.classList.remove('show'); + } + if (this.restartBtn) { + this.restartBtn.style.display = 'none'; + } + // Start questionnaire with the detected context + this.startQuestionnaire(target.dataset.context || null); + // Focus on the component heading + const heading = document.getElementById('detector-title'); + if (heading) { + heading.focus(); + heading.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + break; + } + } + } + }); + } + + private updateProgress(): void { + const totalQuestions = this.questionFlow.length || 5; + const progress = ((this.currentQuestionIndex + 1) / totalQuestions) * 100; + if (this.progressBar) { + this.progressBar.style.width = `${progress}%`; + } + } + + private showQuestion(questionId: string, addToHistory: boolean = true): void { + const questions = this.container.querySelectorAll('.question'); + questions.forEach((q) => q.classList.remove('active')); + + const activeQuestion = this.container.querySelector(`#${questionId}`); + if (activeQuestion) { + activeQuestion.classList.add('active'); + + // Add smart suggestions for URL input question + if (questionId === 'q-url-input') { + this.enhanceUrlInputWithSuggestions(); + } + } + + // Track question history for back navigation + if (addToHistory) { + this.questionHistory.push(questionId); + } + + this.updateProgress(); + } + + private enhanceUrlInputWithSuggestions(): void { + const urlInputQuestion = this.container.querySelector('#q-url-input'); + if (!urlInputQuestion) return; + + const urlInput = urlInputQuestion.querySelector( + '#url-input' + ) as HTMLInputElement; + if (!urlInput) return; + + // Check for existing URL in localStorage + const storedUrls = getInfluxDBUrls(); + const currentProduct = this.getCurrentProduct(); + const storedUrl = storedUrls[currentProduct] || storedUrls.custom; + + if (storedUrl && storedUrl !== 'http://localhost:8086') { + urlInput.value = storedUrl; + // Add indicator that URL was pre-filled (only if one doesn't already exist) + const existingIndicator = urlInput.parentElement?.querySelector( + '.url-prefilled-indicator' + ); + if (!existingIndicator) { + const indicator = document.createElement('div'); + indicator.className = 'url-prefilled-indicator'; + indicator.textContent = 'Using previously saved URL'; + urlInput.parentElement?.insertBefore(indicator, urlInput); + + // Hide indicator when user starts typing + const originalValue = urlInput.value; + urlInput.addEventListener('input', () => { + if (urlInput.value !== originalValue) { + indicator.style.display = 'none'; + } + }); + } + } else { + // Set a basic placeholder suggestion + const suggestedUrl = this.getBasicUrlSuggestion(); + urlInput.placeholder = `for example, ${suggestedUrl}`; + } + } + + private getCurrentProduct(): string { + // Try to determine current product context from page or default + // This could be enhanced to detect from page context + return 'core'; // Default to core for now + } + + private handleUrlKnown(value: string | undefined): void { + this.currentQuestionIndex++; + + if (value === 'true') { + this.showQuestion('q-url-input'); + } else if (value === 'airgapped') { + this.showQuestion('q-ping-manual'); + // Set up placeholder after question is shown + setTimeout(() => this.setupPingHeadersPlaceholder(), 0); + } else if (value === 'docker') { + this.answers.isDocker = true; + this.showQuestion('q-docker-manual'); + // Set up placeholder after question is shown + setTimeout(() => this.setupDockerOutputPlaceholder(), 0); + } else { + // Start the questionnaire + this.answers = {}; + this.questionFlow = ['q-paid', 'q-hosted', 'q-age', 'q-language']; + this.currentQuestionIndex = 0; + this.showQuestion('q-paid'); + } + } + + private goBack(): void { + // Remove current question from history + if (this.questionHistory.length > 0) { + this.questionHistory.pop(); + } + + // Go to previous question if available + if (this.questionHistory.length > 0) { + const previousQuestion = + this.questionHistory[this.questionHistory.length - 1]; + // Remove it from history before showing (showQuestion will re-add it) + this.questionHistory.pop(); + + // Decrement question index + if (this.currentQuestionIndex > 0) { + this.currentQuestionIndex--; + } + + // Show previous question + this.showQuestion(previousQuestion); + } else { + // No history - go to first question + this.currentQuestionIndex = 0; + this.showQuestion('q-url-known'); + } + } + + private async detectByUrl(): Promise { + const urlInput = ( + this.container.querySelector('#url-input') as HTMLInputElement + )?.value.trim(); + + if (!urlInput) { + this.showResult('error', 'Please enter a valid URL'); + return; + } + + // Use improved URL pattern analysis + const analysisResult = this.analyzeUrlPatterns(urlInput); + + // Store URL detection results for scoring system + if (analysisResult.likelyProduct && analysisResult.likelyProduct !== null) { + this.answers.detectedProduct = analysisResult.likelyProduct; + this.answers.detectedConfidence = analysisResult.confidence.toString(); + } + + if (analysisResult.likelyProduct && analysisResult.likelyProduct !== null) { + if (analysisResult.suggestion === 'ping-test') { + // Show ping test suggestion for Core/Enterprise detection + this.showPingTestSuggestion(urlInput, analysisResult.likelyProduct); + return; + } else if (analysisResult.suggestion === 'version-check') { + // Show OSS version check suggestion + this.showOSSVersionCheckSuggestion(urlInput); + return; + } else if (analysisResult.suggestion === 'multiple-candidates-8086') { + // Show multiple product suggestions for port 8086 + this.showMultipleCandidatesSuggestion(urlInput, '8086'); + return; + } else if (analysisResult.suggestion === 'multiple-candidates-8181') { + // Show multiple product suggestions for port 8181 + this.showMultipleCandidatesSuggestion(urlInput, '8181'); + return; + } else { + // Direct detection + this.showDetectedVersion(analysisResult.likelyProduct); + return; + } + } + + // URL not recognized - start questionnaire with context + this.showResult('info', 'Analyzing your InfluxDB server...'); + + // Check if this is a cloud context (like "cloud 2") + const contextResult = this.detectContext(urlInput); + if (contextResult.likelyProduct === 'cloud') { + // Start questionnaire with cloud context + setTimeout(() => { + this.startQuestionnaireWithCloudContext(); + }, 2000); + } else { + // For other URLs, use the regular questionnaire + setTimeout(() => { + this.startQuestionnaire('manual', this.detectPortFromUrl(urlInput)); + }, 2000); + } + } + + private detectContext(urlInput: string): { likelyProduct?: string } { + const input = urlInput.toLowerCase(); + + // Check for cloud indicators + if (input.includes('cloud') || input.includes('influxdata.com')) { + return { likelyProduct: 'cloud' }; + } + + // Check for other patterns like "cloud 2" + if (/cloud\s*[v]?2/.test(input)) { + return { likelyProduct: 'cloud' }; + } + + return {}; + } + + private detectPortFromUrl(urlString: string): string | null { + try { + const url = new URL(urlString); + const port = url.port || (url.protocol === 'https:' ? '443' : '80'); + + if (port === '8181') { + return 'v3'; // InfluxDB 3 Core/Enterprise typically use 8181 + } else if (port === '8086') { + return 'legacy'; // OSS v1/v2 or Enterprise v1 typically use 8086 + } + } catch { + // Invalid URL + } + return null; + } + + private startQuestionnaire( + context: string | null = null, + portClue: string | null = null + ): void { + this.answers = {}; + this.answers.context = context; + this.answers.portClue = portClue; + this.answers.isCloud = false; + this.questionFlow = ['q-paid', 'q-age', 'q-language']; + this.currentQuestionIndex = 0; + this.showQuestion('q-paid'); + } + + private startQuestionnaireWithCloudContext(): void { + this.answers = {}; + this.answers.context = 'cloud'; + this.answers.hosted = 'cloud'; // Pre-set cloud hosting + this.answers.isCloud = true; + this.questionFlow = ['q-paid', 'q-age', 'q-language']; + this.currentQuestionIndex = 0; + this.showQuestion('q-paid'); + } + + private answerQuestion(category: string, answer: string): void { + this.answers[category] = answer; + + // Determine next question or show results + if (category === 'paid') { + if (!this.answers.context) { + // No URL provided - ask about cloud vs self-hosted + this.currentQuestionIndex = 1; + this.showQuestion('q-hosted'); + } else { + // We have context from URL - go to age + this.currentQuestionIndex = 1; + this.showQuestion('q-age'); + } + } else if (category === 'hosted') { + this.currentQuestionIndex = 2; + this.showQuestion('q-age'); + } else if (category === 'age') { + this.currentQuestionIndex = 3; + this.showQuestion('q-language'); + } else if (category === 'language') { + // All questions answered - show ranked results + this.showRankedResults(); + } + } + + private handleAuthorizationHelp(category: string, answer: string): void { + // Store the answer + this.answers[category] = answer; + + // Check if we're in the context of localhost:8181 detection + // If so, we can provide a high-confidence result + const currentUrl = + ( + this.container.querySelector('#url-input') as HTMLInputElement + )?.value?.toLowerCase() || ''; + const isLocalhost8181 = + (currentUrl.includes('localhost') || currentUrl.includes('127.0.0.1')) && + currentUrl.includes(':8181'); + + if (isLocalhost8181) { + // For localhost:8181, we can give high-confidence results based on license + if (answer === 'free') { + // High confidence it's InfluxDB 3 Core + const html = ` + Based on your localhost:8181 server and free license:

+ ${this.generateProductResult('core', true, 'High', false)} +
+ Want to confirm this result? + +
+ `; + this.showResult('success', html); + } else if (answer === 'paid') { + // High confidence it's InfluxDB 3 Enterprise + const html = ` + Based on your localhost:8181 server and paid license:

+ ${this.generateProductResult('enterprise', true, 'High', false)} +
+ Want to confirm this result? + +
+ `; + this.showResult('success', html); + } + } else { + // Original behavior for non-localhost:8181 cases + const resultDiv = this.container.querySelector('#result'); + if (resultDiv) { + // Add a message about what the license answer means + const licenseGuidance = document.createElement('div'); + licenseGuidance.className = 'license-guidance'; + licenseGuidance.style.marginTop = '1rem'; + licenseGuidance.style.padding = '0.75rem'; + licenseGuidance.style.backgroundColor = + 'rgba(var(--article-link-rgb, 0, 163, 255), 0.1)'; + licenseGuidance.style.borderLeft = + '4px solid var(--article-link, #00A3FF)'; + licenseGuidance.style.borderRadius = '4px'; + + if (answer === 'free') { + licenseGuidance.innerHTML = ` + Free/Open Source License: +

This suggests you're using InfluxDB 3 Core or InfluxDB OSS.

+ + `; + } else if (answer === 'paid') { + licenseGuidance.innerHTML = ` + Paid/Commercial License: +

This suggests you're using InfluxDB 3 Enterprise or a paid cloud service.

+ + `; + } + + // Remove any existing guidance + const existingGuidance = resultDiv.querySelector('.license-guidance'); + if (existingGuidance) { + existingGuidance.remove(); + } + + // Add the new guidance + resultDiv.appendChild(licenseGuidance); + + // Focus on the guidance message for accessibility + licenseGuidance.focus(); + } + } + } + + private showRankedResults(): void { + const scores: Record = {}; + + // Initialize all products with base score using their full display names + // The scoring logic uses full names like 'InfluxDB 3 Core', not keys like 'influxdb3_core' + Object.entries(this.products).forEach(([key, config]) => { + const fullName = config.name || key; + scores[fullName] = 0; + }); + + // Apply scoring logic based on answers + this.applyScoring(scores); + + // Check if user answered "unknown" to all questions + const allUnknown = + (!this.answers.paid || this.answers.paid === 'unknown') && + (!this.answers.hosted || this.answers.hosted === 'unknown') && + (!this.answers.age || this.answers.age === 'unknown') && + (!this.answers.language || this.answers.language === 'unknown'); + + // Sort by score and filter out vague products + const ranked = Object.entries(scores) + .filter(([product, score]) => { + // Filter by score threshold + if (score <= -50) return false; + // Exclude generic "InfluxDB" product (too vague for results) + if (product === 'InfluxDB') return false; + return true; + }) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5); + + // Display results + this.displayRankedResults(ranked, allUnknown); + } + + /** + * Gets the Grafana documentation link for a given product + */ + private getGrafanaLink(productName: string): string | null { + const GRAFANA_LINKS: Record = { + 'InfluxDB 3 Core': '/influxdb3/core/visualize-data/grafana/', + 'InfluxDB 3 Enterprise': '/influxdb3/enterprise/visualize-data/grafana/', + 'InfluxDB Cloud Dedicated': + '/influxdb3/cloud-dedicated/visualize-data/grafana/', + 'InfluxDB Cloud Serverless': + '/influxdb3/cloud-serverless/visualize-data/grafana/', + 'InfluxDB OSS 1.x': '/influxdb/v1/tools/grafana/', + 'InfluxDB OSS 2.x': '/influxdb/v2/visualize-data/grafana/', + 'InfluxDB Enterprise': '/influxdb/enterprise/visualize-data/grafana/', + 'InfluxDB Clustered': '/influxdb3/clustered/visualize-data/grafana/', + 'InfluxDB Cloud (TSM)': '/influxdb/cloud/visualize-data/grafana/', + 'InfluxDB Cloud v1': '/influxdb/cloud/visualize-data/grafana/', + }; + + return GRAFANA_LINKS[productName] || null; + } + + /** + * Generates a unified product result block with characteristics and Grafana link + */ + private generateProductResult( + productName: string, + isTopResult: boolean = false, + confidence?: string, + showRanking?: boolean + ): string { + const displayName = this.getProductDisplayName(productName) || productName; + const grafanaLink = this.getGrafanaLink(displayName); + const resultClass = isTopResult + ? 'product-ranking top-result' + : 'product-ranking'; + + // Get characteristics from products data + const characteristics = this.products[productName]?.characteristics; + + let html = `
`; + + if (showRanking) { + html += `
${displayName}
`; + if (isTopResult) { + html += 'Most Likely'; + } + } else { + html += `
${displayName}
`; + if (isTopResult) { + html += 'Detected'; + } + } + + // Add characteristics and confidence + const details = []; + if (confidence) details.push(`Confidence: ${confidence}`); + if (characteristics) { + details.push(characteristics.slice(0, 3).join(', ')); + } + + if (details.length > 0) { + html += `
${details.join(' • ')}
`; + } + + // Add Grafana link if available + if (grafanaLink) { + html += ` + + `; + } + + html += '
'; + + // Add configuration guidance for top results + if (isTopResult) { + const configGuidance = this.generateConfigurationGuidance(productName); + if (configGuidance) { + html += configGuidance; + } + } + + return html; + } + + /** + * Maps simple product keys (used in URL detection) to full product names (used in scoring) + */ + private mapProductKeyToFullName(productKey: string): string | null { + const KEY_TO_FULL_NAME_MAP: Record = { + core: 'InfluxDB 3 Core', + enterprise: 'InfluxDB 3 Enterprise', + serverless: 'InfluxDB Cloud Serverless', + dedicated: 'InfluxDB Cloud Dedicated', + clustered: 'InfluxDB Clustered', + 'cloud-v2-tsm': 'InfluxDB Cloud (TSM)', + 'cloud-v1': 'InfluxDB Cloud v1', + oss: 'InfluxDB OSS 2.x', + 'oss-1x': 'InfluxDB OSS 1.x', + 'enterprise-1x': 'InfluxDB Enterprise', + }; + + return KEY_TO_FULL_NAME_MAP[productKey] || null; + } + + private applyScoring(scores: Record): void { + // Product release dates for time-aware scoring + const PRODUCT_RELEASE_DATES: Record = { + 'InfluxDB 3 Core': new Date('2025-01-01'), + 'InfluxDB 3 Enterprise': new Date('2025-01-01'), + 'InfluxDB Cloud Serverless': new Date('2024-01-01'), + 'InfluxDB Cloud Dedicated': new Date('2024-01-01'), + 'InfluxDB Clustered': new Date('2024-01-01'), + 'InfluxDB OSS 2.x': new Date('2020-11-01'), + 'InfluxDB Cloud (TSM)': new Date('2020-11-01'), + 'InfluxDB OSS 1.x': new Date('2016-09-01'), + 'InfluxDB Enterprise': new Date('2016-09-01'), + }; + + const currentDate = new Date(); + + // Apply URL detection boost if available + if (this.answers.detectedProduct && this.answers.detectedConfidence) { + const detectedProduct = this.answers.detectedProduct as string; + const confidence = + typeof this.answers.detectedConfidence === 'number' + ? this.answers.detectedConfidence + : parseFloat(this.answers.detectedConfidence as string); + + // Determine confidence boost value + let boostValue = 0; + if (confidence >= 1.0) { + boostValue = 100; // Definitive match + } else if (confidence >= 0.9) { + boostValue = 80; // Very high confidence + } else if (confidence >= 0.7) { + boostValue = 60; // High confidence + } else if (confidence >= 0.5) { + boostValue = 40; // Medium confidence + } + + // Handle special case: 'core or enterprise' should boost BOTH products equally + if (detectedProduct === 'core or enterprise') { + scores['InfluxDB 3 Core'] += boostValue; + scores['InfluxDB 3 Enterprise'] += boostValue; + } else { + // Normal case: boost single detected product + const fullProductName = this.mapProductKeyToFullName(detectedProduct); + if (fullProductName && scores[fullProductName] !== undefined) { + scores[fullProductName] += boostValue; + } + } + } + + // Cloud vs self-hosted + if (this.answers.hosted === 'cloud') { + scores['InfluxDB 3 Core'] = -1000; + scores['InfluxDB 3 Enterprise'] = -1000; + scores['InfluxDB OSS 1.x'] = -1000; + scores['InfluxDB OSS 2.x'] = -1000; + scores['InfluxDB Enterprise'] = -1000; + scores['InfluxDB Clustered'] = -1000; + } else if (this.answers.hosted === 'self' || !this.answers.isCloud) { + scores['InfluxDB Cloud Dedicated'] = -1000; + scores['InfluxDB Cloud Serverless'] = -1000; + scores['InfluxDB Cloud (TSM)'] = -1000; + } + + // Paid vs Free + if (this.answers.paid === 'free') { + scores['InfluxDB 3 Core'] += 25; + scores['InfluxDB OSS 1.x'] += 25; + scores['InfluxDB OSS 2.x'] += 25; + scores['InfluxDB'] += 25; // Generic InfluxDB (OSS v2.x) + scores['InfluxDB Cloud Serverless'] += 10; + scores['InfluxDB Cloud (TSM)'] += 10; + + scores['InfluxDB 3 Enterprise'] = -1000; + scores['InfluxDB Enterprise'] = -1000; + scores['InfluxDB Clustered'] = -1000; + scores['InfluxDB Cloud Dedicated'] = -1000; + } else if (this.answers.paid === 'paid') { + scores['InfluxDB 3 Enterprise'] += 25; + scores['InfluxDB Enterprise'] += 20; + scores['InfluxDB Clustered'] += 15; + scores['InfluxDB Cloud Dedicated'] += 20; + scores['InfluxDB Cloud Serverless'] += 15; + scores['InfluxDB Cloud (TSM)'] += 15; + + scores['InfluxDB 3 Core'] = -1000; + scores['InfluxDB OSS 1.x'] = -1000; + scores['InfluxDB OSS 2.x'] = -1000; + scores['InfluxDB'] = -1000; // Generic InfluxDB (OSS v2.x) + } + + // Time-aware age-based scoring + Object.entries(scores).forEach(([product]) => { + const releaseDate = PRODUCT_RELEASE_DATES[product]; + if (!releaseDate) return; + + const yearsSinceRelease = + (currentDate.getTime() - releaseDate.getTime()) / + (365.25 * 24 * 60 * 60 * 1000); + + if (this.answers.age === 'recent') { + // Favor products released within last year + if (yearsSinceRelease < 1) { + scores[product] += 40; // Very new product + } else if (yearsSinceRelease < 3) { + scores[product] += 25; // Relatively new + } + } else if (this.answers.age === '1-5') { + // Check if product existed in this timeframe + if (yearsSinceRelease >= 1 && yearsSinceRelease <= 5) { + scores[product] += 25; + } else if (yearsSinceRelease < 1) { + scores[product] -= 30; // Too new for this age range + } + } else if (this.answers.age === '5+') { + // Only penalize if product didn't exist 5+ years ago + if (yearsSinceRelease < 5) { + scores[product] -= 100; // Product didn't exist 5 years ago + } else { + scores[product] += 30; // Product was available 5+ years ago + } + } + }); + + // Query language scoring + if (this.answers.language === 'sql') { + scores['InfluxDB 3 Core'] += 40; + scores['InfluxDB 3 Enterprise'] += 40; + scores['InfluxDB Cloud Dedicated'] += 30; + scores['InfluxDB Cloud Serverless'] += 30; + scores['InfluxDB Clustered'] += 30; + + scores['InfluxDB OSS 1.x'] = -1000; + scores['InfluxDB OSS 2.x'] = -1000; + scores['InfluxDB'] = -1000; // Generic InfluxDB (OSS v2.x) + scores['InfluxDB Enterprise'] = -1000; + scores['InfluxDB Cloud (TSM)'] = -1000; + } else if (this.answers.language === 'flux') { + scores['InfluxDB OSS 2.x'] += 30; + scores['InfluxDB'] += 30; // Generic InfluxDB (OSS v2.x) + scores['InfluxDB Cloud (TSM)'] += 40; + scores['InfluxDB Cloud Serverless'] += 20; + scores['InfluxDB Enterprise'] += 20; // v1.x Enterprise supports Flux + + scores['InfluxDB OSS 1.x'] = -1000; + scores['InfluxDB 3 Core'] = -1000; + scores['InfluxDB 3 Enterprise'] = -1000; + scores['InfluxDB Cloud Dedicated'] = -1000; + scores['InfluxDB Clustered'] = -1000; + } else if (this.answers.language === 'influxql') { + // InfluxQL is supported by all products except pure Flux products + scores['InfluxDB OSS 1.x'] += 30; + scores['InfluxDB Enterprise'] += 30; + scores['InfluxDB OSS 2.x'] += 20; + scores['InfluxDB'] += 20; // Generic InfluxDB (OSS v2.x) + scores['InfluxDB Cloud (TSM)'] += 20; + scores['InfluxDB 3 Core'] += 25; + scores['InfluxDB 3 Enterprise'] += 25; + scores['InfluxDB Cloud Dedicated'] += 25; + scores['InfluxDB Cloud Serverless'] += 25; + scores['InfluxDB Clustered'] += 25; + } + } + + private displayRankedResults( + ranked: [string, number][], + allUnknown: boolean = false + ): void { + const topScore = ranked[0]?.[1] || 0; + const secondScore = ranked[1]?.[1] || 0; + const hasStandout = topScore > 30 && topScore - secondScore >= 15; + + let html = ''; + + // If all answers were "I'm not sure", show a helpful message + if (allUnknown) { + html = + 'Unable to determine your InfluxDB product

' + + '

Since you answered "I\'m not sure" to all questions, we don\'t have enough information to identify your InfluxDB product.

' + + '

Please check the InfluxDB version quick reference table below to identify your product based on its characteristics.


'; + } else { + html = + 'Based on your answers, here are the most likely InfluxDB products:

'; + } + + // Only show ranked products if we have meaningful answers + if (!allUnknown) { + ranked.forEach(([product, score], index) => { + const confidence = score > 60 ? 'High' : score > 30 ? 'Medium' : 'Low'; + const isTopResult = index === 0 && hasStandout; + + // Use unified product result generation with ranking number + let productHtml = this.generateProductResult( + product, + isTopResult, + confidence, + true + ); + + // Add ranking number to the product title + productHtml = productHtml.replace( + '
', + `
${index + 1}. ` + ); + + html += productHtml; + }); + } + + // Add Quick Reference table (open by default if all answers unknown) + html += ` +
+ + + InfluxDB version quick reference + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProductLicenseHostingPortPing requires authQuery languages
InfluxDB 3 EnterprisePaid onlySelf-hosted8181Yes (opt-out)SQL, InfluxQL
InfluxDB 3 CoreFree onlySelf-hosted8181Yes (opt-out)SQL, InfluxQL
InfluxDB EnterprisePaid onlySelf-hosted8086Yes (required)InfluxQL, Flux
InfluxDB ClusteredPaid onlySelf-hostedVariesNoSQL, InfluxQL
InfluxDB OSS 1.xFree onlySelf-hosted8086No (optional)InfluxQL
InfluxDB OSS 2.xFree onlySelf-hosted8086NoInfluxQL, Flux
InfluxDB Cloud DedicatedPaid onlyCloudN/ANoSQL, InfluxQL
InfluxDB Cloud ServerlessFree + PaidCloudN/AN/ASQL, InfluxQL, Flux
InfluxDB Cloud (TSM)Free + PaidCloudN/AN/AInfluxQL, Flux
+ +
+ `; + + this.showResult('success', html); + } + + private analyzePingHeaders(): void { + const headersText = ( + this.container.querySelector('#ping-headers') as HTMLTextAreaElement + )?.value.trim(); + + if (!headersText) { + this.showResult('error', 'Please paste the ping response headers'); + return; + } + + // Check if user is trying to analyze the example content + if ( + headersText.includes( + '# Replace this with your actual response headers' + ) || + headersText.includes('# Example formats:') + ) { + this.showResult( + 'error', + 'Please replace the example content with your actual ping response headers' + ); + return; + } + + // Check for 401/403 unauthorized responses + if (headersText.includes('401') || headersText.includes('403')) { + this.showResult( + 'info', + ` + Authentication Required Detected

+ The ping endpoint requires authentication, which indicates you're likely using one of:

+
+
+ InfluxDB 3 Enterprise - Requires auth by default (opt-out possible) +
+
+ InfluxDB 3 Core - Requires auth by default (opt-out possible) +
+
+ Please use the guided questions to narrow down your specific version. + ` + ); + return; + } + + // Parse headers and check against patterns + const headers: Record = {}; + headersText.split('\n').forEach((line) => { + const colonIndex = line.indexOf(':'); + if (colonIndex > -1) { + const key = line.substring(0, colonIndex).trim().toLowerCase(); + const value = line.substring(colonIndex + 1).trim(); + headers[key] = value; + } + }); + + // PRIORITY: Check for definitive x-influxdb-build header (per decision tree) + const buildHeader = headers['x-influxdb-build']; + if (buildHeader) { + if (buildHeader.toLowerCase().includes('enterprise')) { + this.showDetectedVersion('InfluxDB 3 Enterprise'); + return; + } else if (buildHeader.toLowerCase().includes('core')) { + this.showDetectedVersion('InfluxDB 3 Core'); + return; + } + } + + // Check against product patterns + let detectedProduct: string | null = null; + for (const [productName, config] of Object.entries(this.products)) { + if (config.detection?.ping_headers) { + let matches = true; + for (const [header, pattern] of Object.entries( + config.detection.ping_headers + )) { + const regex = new RegExp(pattern); + if (!headers[header] || !regex.test(headers[header])) { + matches = false; + break; + } + } + if (matches) { + detectedProduct = productName; + break; + } + } + } + + if (detectedProduct) { + this.showDetectedVersion(detectedProduct); + } else { + this.showResult( + 'warning', + 'Unable to determine version from headers. Consider using the guided questions instead.' + ); + } + } + + private showResult(type: string, message: string): void { + if (this.resultDiv) { + this.resultDiv.className = `result ${type} show`; + this.resultDiv.innerHTML = message; + } + if (this.restartBtn) { + this.restartBtn.style.display = 'block'; + } + } + + private analyzeDockerOutput(): void { + const dockerOutput = ( + this.container.querySelector('#docker-output') as HTMLTextAreaElement + )?.value.trim(); + + if (!dockerOutput) { + this.showResult('error', 'Please paste the Docker command output'); + return; + } + + // Check if user is trying to analyze the example content + if ( + dockerOutput.includes('# Replace this with your actual command output') || + dockerOutput.includes('# Example formats:') + ) { + this.showResult( + 'error', + 'Please replace the example content with your actual Docker command output' + ); + return; + } + + let detectedProduct: string | null = null; + + // Check for version patterns in the output + if (dockerOutput.includes('InfluxDB 3 Core')) { + detectedProduct = 'InfluxDB 3 Core'; + } else if (dockerOutput.includes('InfluxDB 3 Enterprise')) { + detectedProduct = 'InfluxDB 3 Enterprise'; + } else if (dockerOutput.includes('InfluxDB v3')) { + // Generic v3 detection - need more info + detectedProduct = 'InfluxDB 3 Core or Enterprise'; + } else if ( + dockerOutput.includes('InfluxDB v2') || + dockerOutput.includes('InfluxDB 2.') + ) { + detectedProduct = 'InfluxDB OSS 2.x'; + } else if ( + dockerOutput.includes('InfluxDB v1') || + dockerOutput.includes('InfluxDB 1.') + ) { + if (dockerOutput.includes('Enterprise')) { + detectedProduct = 'InfluxDB Enterprise'; + } else { + detectedProduct = 'InfluxDB OSS 1.x'; + } + } + + // Also check for ping header patterns (case-insensitive) + if (!detectedProduct) { + // First check for x-influxdb-build header (definitive identification) + const buildMatch = dockerOutput.match(/x-influxdb-build:\s*(\w+)/i); + if (buildMatch) { + const build = buildMatch[1].toLowerCase(); + if (build === 'enterprise') { + detectedProduct = 'InfluxDB 3 Enterprise'; + } else if (build === 'core') { + detectedProduct = 'InfluxDB 3 Core'; + } + } + + // If no build header, check version headers (case-insensitive) + if (!detectedProduct) { + const versionMatch = dockerOutput.match( + /x-influxdb-version:\s*([\d.]+)/i + ); + if (versionMatch) { + const version = versionMatch[1]; + if (version.startsWith('3.')) { + detectedProduct = 'InfluxDB 3 Core or InfluxDB 3Enterprise'; + } else if (version.startsWith('2.')) { + detectedProduct = 'InfluxDB OSS 2.x'; + } else if (version.startsWith('1.')) { + detectedProduct = dockerOutput.includes('Enterprise') + ? 'InfluxDB Enterprise' + : 'InfluxDB OSS 1.x'; + } + } + } + } + + if (detectedProduct) { + this.showDetectedVersion(detectedProduct); + } else { + this.showResult( + 'warning', + 'Unable to determine version from Docker output. Consider using the guided questions instead.' + ); + } + } + + private showPingTestSuggestion(url: string, productName: string): void { + // Convert product key to display name + const displayName = this.getProductDisplayName(productName) || productName; + const html = ` + Port 8181 detected - likely ${displayName}

+ +

To distinguish between InfluxDB 3 Core and Enterprise, run one of these commands:

+ +
+# Direct API call: +curl -I ${url}/ping +
+ +
+ + View Docker/Container Commands + +
+# With Docker Compose: +docker compose exec influxdb3 curl -I http://localhost:8181/ping + +# With Docker (replace <container> with your container name): +docker exec <container> curl -I localhost:8181/ping +
+
+ +
+
Expected results:
+ • X-Influxdb-Build: Enterprise → InfluxDB 3 Enterprise (definitive)
+ • X-Influxdb-Build: Core → InfluxDB 3 Core (definitive)
+ • 401 Unauthorized → Use the license information below +
+ +
+
If you get 401 Unauthorized:
+

What type of license do you have?

+ + +
+ +
+ Can't run the command? + +
+ `; + this.showResult('success', html); + } + + private showOSSVersionCheckSuggestion(url: string): void { + const html = ` + Port 8086 detected - likely InfluxDB OSS

+ +

To determine if this is InfluxDB OSS v1.x or v2.x, run one of these commands:

+ +
+# Check version directly: +influxd version + +# Or check via API: +curl -I ${url}/ping +
+ +
+
Expected version patterns:
+ • v1.x.x → ${this.getProductDisplayName('oss-v1')}
+ • v2.x.x → ${this.getProductDisplayName('oss-v2')}
+
+ +
+ + Docker/Container Commands + +
+# Get version info: +docker exec <container> influxd version + +# Get ping headers: +docker exec <container> curl -I localhost:8086/ping + +# Or check startup logs: +docker logs <container> 2>&1 | head -20 +
+

+ Replace <container> with your actual container name or ID. +

+
+ +
+ Can't run these commands? + +
+ `; + this.showResult('success', html); + } + + private showMultipleCandidatesSuggestion(url: string, port: string): void { + let candidates: string[] = []; + let portDescription = ''; + + if (port === '8086') { + candidates = [ + 'InfluxDB OSS 1.x', + 'InfluxDB OSS 2.x', + 'InfluxDB Enterprise', + ]; + portDescription = + 'Port 8086 is used by InfluxDB OSS v1.x, OSS v2.x, and Enterprise v1.x'; + } else if (port === '8181') { + candidates = ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise']; + portDescription = 'Port 8181 is used by InfluxDB 3 Core and Enterprise'; + } + + const candidatesList = candidates + .map((product) => + this.generateProductResult(product, false, 'Medium', false) + ) + .join(''); + + const html = ` + Based on the port pattern in your URL, here are the possible products:

+ +

${portDescription}. Without additional information, we cannot determine which specific version you're using.

+ +
+ Possible products:
+ ${candidatesList} +
+ +
+ To narrow this down: + +
+ `; + this.showResult('info', html); + } + + private showDetectedVersion(productName: string): void { + // Track successful detection + this.trackAnalyticsEvent({ + interaction_type: 'product_detected', + detected_product: productName.toLowerCase().replace(/\s+/g, '_'), + completion_status: 'success', + section: this.getCurrentPageSection(), + }); + + const html = ` + Based on your input, we believe the InfluxDB product you are using is most likely:

+ ${this.generateProductResult(productName, true, 'High', false)} + `; + this.showResult('success', html); + } + + private restart(): void { + this.answers = {}; + this.questionFlow = []; + this.currentQuestionIndex = 0; + this.questionHistory = []; + + // Clear inputs + const urlInput = this.container.querySelector( + '#url-input' + ) as HTMLInputElement; + const pingHeaders = this.container.querySelector( + '#ping-headers' + ) as HTMLTextAreaElement; + const dockerOutput = this.container.querySelector( + '#docker-output' + ) as HTMLTextAreaElement; + + if (urlInput) urlInput.value = ''; + if (pingHeaders) pingHeaders.value = ''; + if (dockerOutput) dockerOutput.value = ''; + + // Remove URL prefilled indicator if present + const indicator = this.container.querySelector('.url-prefilled-indicator'); + if (indicator) { + indicator.remove(); + } + + // Hide result + if (this.resultDiv) { + this.resultDiv.classList.remove('show'); + } + if (this.restartBtn) { + this.restartBtn.style.display = 'none'; + } + + // Show first question + this.showQuestion('q-url-known'); + + // Reset progress + if (this.progressBar) { + this.progressBar.style.width = '0%'; + } + } +} + +// Export as component initializer +export default function initInfluxDBVersionDetector( + options: ComponentOptions +): InfluxDBVersionDetector { + return new InfluxDBVersionDetector(options); +} diff --git a/assets/js/main.js b/assets/js/main.js index ca99dff48..c1b8a7088 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -35,6 +35,7 @@ import DocSearch from './components/doc-search.js'; import FeatureCallout from './feature-callouts.js'; import FluxGroupKeysDemo from './flux-group-keys.js'; import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js'; +import InfluxDBVersionDetector from './influxdb-version-detector.ts'; import KeyBinding from './keybindings.js'; import ListFilters from './list-filters.js'; import ProductSelector from './version-selector.js'; @@ -64,6 +65,7 @@ const componentRegistry = { 'feature-callout': FeatureCallout, 'flux-group-keys-demo': FluxGroupKeysDemo, 'flux-influxdb-versions-trigger': FluxInfluxDBVersionsTrigger, + 'influxdb-version-detector': InfluxDBVersionDetector, keybinding: KeyBinding, 'list-filters': ListFilters, 'product-selector': ProductSelector, @@ -113,7 +115,10 @@ function initComponents(globals) { if (ComponentConstructor) { // Initialize the component and store its instance in the global namespace try { - const instance = ComponentConstructor({ component }); + // Prepare component options + const options = { component }; + + const instance = ComponentConstructor(options); globals[componentName] = ComponentConstructor; // Optionally store component instances for future reference diff --git a/assets/styles/components/_influxdb-version-detector.scss b/assets/styles/components/_influxdb-version-detector.scss new file mode 100644 index 000000000..c64d3f21d --- /dev/null +++ b/assets/styles/components/_influxdb-version-detector.scss @@ -0,0 +1,647 @@ +// InfluxDB Version Detector Component Styles + +.influxdb-version-detector { + // CSS Custom Properties + --transition-fast: 0.2s ease; + --transition-normal: 0.3s ease; + --spacing-sm: 0.625rem; + --spacing-md: 1.25rem; + + margin: 2rem auto; + + .detector-title { + color: $article-heading; + margin-bottom: 0.625rem; + font-size: 1.8em; + font-weight: 600; + } + + .detector-subtitle { + color: $article-text; + margin-bottom: 1.875rem; + font-size: 0.95em; + opacity: 0.8; + } + + // Progress bar + .progress { + margin-bottom: 1.5625rem; + height: 6px; + background: $article-hr; + border-radius: 3px; + overflow: hidden; + + .progress-bar { + height: 100%; + background: $article-link; + transition: width var(--transition-normal); + } + } + + // Question container + .question-container { + min-height: 150px; + + .question { + display: none; + animation: fadeIn var(--transition-normal); + + &.active { + display: block; + } + + .question-text { + font-size: 1.1em; + color: $article-heading; + margin-bottom: 1.25rem; + font-weight: 500; + } + } + } + + // Buttons - Base styles and variants + %button-base { + border: none; + border-radius: var(--border-radius); + cursor: pointer; + transition: all var(--transition-fast); + font-family: inherit; + + &:focus { + outline: 2px solid $article-link; + outline-offset: 2px; + } + } + + .option-button { + @extend %button-base; + display: block; + width: 100%; + text-align: left; + margin-bottom: 0.75rem; + padding: 0.875rem 1.125rem; + background: $article-bg; + color: $article-text; + border: 2px solid $article-hr; + font-size: 15px; + + &:hover { + border-color: $article-link; + background: $article-bg; + transform: translateX(3px); + } + + &:active { + transform: translateX(1px); + } + } + + .submit-button { + @extend %button-base; + background: $article-link; + color: $g20-white; + padding: 0.75rem 1.5rem; + font-size: 15px; + font-weight: 500; + + &:hover { + background: $b-ocean; + color: $g20-white; + } + + &:disabled { + background: $g8-storm; + cursor: not-allowed; + } + } + + .back-button { + @extend %button-base; + background: $g8-storm; + color: $g20-white; + padding: var(--spacing-sm) var(--spacing-md); + font-size: 14px; + margin-right: var(--spacing-sm); + + &:hover { + background: $g9-mountain; + } + } + + .restart-button { + @extend .back-button; + margin-top: var(--spacing-md); + margin-right: 0; + } + + // Input fields + %input-base { + width: 100%; + border: 2px solid $article-hr; + border-radius: var(--border-radius); + transition: border-color var(--transition-fast); + background: $article-bg; + color: $article-text; + + &:focus { + outline: none; + border-color: $article-link; + } + } + + .input-group { + margin-bottom: var(--spacing-md); + + label { + display: block; + margin-bottom: 0.5rem; + color: $article-text; + font-weight: 500; + } + + input { + @extend %input-base; + padding: 0.75rem; + font-size: 14px; + } + } + + textarea { + @extend %input-base; + padding: var(--spacing-sm); + font-family: var(--font-mono, 'Courier New', monospace); + font-size: 12px; + resize: vertical; + min-height: 120px; + + &::placeholder { + color: rgba($article-text, 0.6); + opacity: 1; // Firefox fix + } + + &::-webkit-input-placeholder { + color: rgba($article-text, 0.6); + } + + &::-moz-placeholder { + color: rgba($article-text, 0.6); + opacity: 1; + } + + &:-ms-input-placeholder { + color: rgba($article-text, 0.6); + } + } + + // Code block - match site standards + .code-block { + background: $article-code-bg; + color: $article-code; + padding: 1.75rem 1.75rem 1.25rem; + border-radius: $radius; + font-family: $code; + font-size: 1rem; + margin: 2rem 0 2.25rem; + overflow-x: scroll; + overflow-y: hidden; + line-height: 1.7rem; + white-space: pre; + } + + // URL pattern hint + .url-pattern-hint { + margin-bottom: var(--spacing-sm); + padding: var(--spacing-sm); + background: $article-note-base; + border: 1px solid $article-note-base; + border-radius: var(--border-radius); + color: $article-note-text; + font-size: 13px; + } + + // URL suggestions + .url-suggestions { + margin-bottom: var(--spacing-md); + + .suggestions-header { + color: $article-heading; + margin-bottom: var(--spacing-sm); + font-size: 14px; + } + + .suggestion-button { + @extend %button-base; + display: block; + width: 100%; + text-align: left; + margin-bottom: var(--spacing-sm); + padding: var(--spacing-sm); + background: $article-bg; + border: 1px solid $article-hr; + + &:hover { + border-color: $article-link; + background: $article-bg; + } + + .suggestion-url { + font-family: var(--font-mono, 'Courier New', monospace); + font-size: 13px; + color: $article-link; + margin-bottom: 2px; + } + + .suggestion-product { + font-size: 12px; + color: $article-text; + opacity: 0.8; + } + + .suggestion-pattern { + font-size: 11px; + color: $article-link; + font-style: italic; + margin-top: 2px; + } + } + } + + // Results + .result { + display: none; + margin-top: var(--spacing-sm); + padding: var(--spacing-md); + border-radius: var(--border-radius); + animation: fadeIn var(--transition-normal); + + &.show { + display: block; + } + + &.success { + background: $article-bg; + border-left: 3px solid $article-note-base; + color: $article-text; + } + + &.error { + background: $r-flan; + border-left: 3px solid $article-caution-base; + color: $r-basalt; + } + + &.info { + background: $article-note-base; + border-left: 3px solid $article-note-base; + color: $article-note-text; + } + + &.warning { + background: $article-warning-bg; + border-left: 3px solid $article-warning-base; + color: $article-warning-text; + } + } + + .detected-version { + font-size: 1.3em; + font-weight: bold; + color: $article-link; + margin-bottom: var(--spacing-sm); + padding: var(--spacing-sm); + background: rgba($article-link, 0.1); + border-radius: 4px; + border-left: 4px solid $article-link; + } + + // URL pre-filled indicator + .url-prefilled-indicator { + font-size: 0.85em; + color: $article-note-text; + margin-bottom: 8px; + padding: 4px 8px; + background: rgba($article-link, 0.1); + border-left: 3px solid $article-link; + } + + // Loading animation + .loading { + display: inline-block; + margin-left: var(--spacing-sm); + + &:after { + content: '...'; + animation: dots 1.5s steps(4, end) infinite; + } + } + + @keyframes dots { + 0%, 20% { + content: '.'; + } + 40% { + content: '..'; + } + 60%, 100% { + content: '...'; + } + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + + // Responsive design + @media (max-width: 768px) { + padding: 1.5rem; + + .detector-title { + font-size: 1.5em; + } + + .option-button { + padding: 0.75rem 1rem; + font-size: 14px; + } + + .submit-button, + .back-button { + padding: var(--spacing-sm) var(--spacing-md); + font-size: 14px; + } + } + + @media (max-width: 480px) { + padding: 1rem; + + .detector-title { + font-size: 1.3em; + } + + .detector-subtitle { + font-size: 0.9em; + } + + .question-text { + font-size: 1em; + } + } + + + // Product ranking results + .product-ranking { + margin-bottom: var(--spacing-sm); + padding: 0.75rem; + border-radius: var(--border-radius); + border-left: 4px solid $article-hr; + background: $article-bg; + + &.top-result { + background: rgba($article-link, 0.1); + border-color: $article-link; + } + + .product-title { + font-weight: 600; + margin-bottom: 0.25rem; + } + + .most-likely-label { + color: $article-link; + font-size: 0.9em; + margin-left: var(--spacing-sm); + } + + .product-details { + color: $article-text; + font-size: 0.9em; + margin-top: 0.25rem; + opacity: 0.8; + } + } + + // Grafana networking tips + .grafana-tips { + margin-top: var(--spacing-md); + padding: 1rem; + background: rgba($article-link, 0.1); + border-left: 4px solid $article-link; + border-radius: var(--border-radius); + + .tips-title { + margin: 0 0 var(--spacing-sm) 0; + color: $article-link; + font-size: 1.1em; + } + + .tips-description { + margin: 0 0 var(--spacing-sm) 0; + font-size: 0.9em; + } + + .tips-list { + margin: 0; + padding-left: 1.25rem; + font-size: 0.85em; + + li { + margin-bottom: 0.25rem; + } + + code { + background: rgba($article-link, 0.15); + padding: 0.125rem 0.25rem; + border-radius: 3px; + font-size: 0.9em; + } + } + + .tips-link { + margin: var(--spacing-sm) 0 0 0; + font-size: 0.85em; + } + } + + // Expected results section + .expected-results { + margin: 1rem 0; + + .results-title { + font-weight: 600; + margin-bottom: 0.5rem; + } + + .results-list { + margin: 0; + padding-left: 1rem; + font-size: 0.9em; + + li { + margin-bottom: 0.25rem; + } + } + } + + // Question text styling + .question-text-spaced { + margin-top: 1rem; + font-weight: normal; + font-size: 0.95em; + } + + .question-options { + margin-top: 1rem; + } + + // Command help section + .command-help { + margin-top: var(--spacing-md); + } + + // Grafana links styling + .grafana-link { + color: $article-link; + text-decoration: underline; + + &:hover { + color: $article-link-hover; + } + } + + // Manual command output + .manual-output { + margin: 1rem 0; + padding: var(--spacing-sm); + background: $article-bg; + border-left: 4px solid $article-link; + border-radius: var(--border-radius); + } + + // Action section with buttons + .action-section { + margin-top: var(--spacing-md); + } + + // Quick Reference expandable section + .quick-reference { + margin-top: 2rem; + + details { + border: 1px solid $article-hr; + border-radius: var(--border-radius); + padding: 0.5rem; + } + + .reference-summary { + cursor: pointer; + font-weight: 600; + padding: 0.5rem 0; + user-select: none; + color: $article-link; + + &:hover { + color: $article-link-hover; + } + } + } + + // Expandable summary styling (for Docker Commands, etc.) + .expandable-summary { + cursor: pointer; + font-weight: 600; + padding: 0.5rem 0; + user-select: none; + color: $article-link; + position: relative; + padding-left: 1.5rem; // Make room for custom icon + + &:hover { + color: $article-link-hover; + } + + // Hide the default disclosure triangle + &::marker, + &::-webkit-details-marker { + display: none; + } + + // Add custom plus/minus icon + &::before { + content: '+'; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 1rem; + height: 1rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + color: $article-link; + border: 1px solid $article-link; + border-radius: 3px; + background: transparent; + } + + // Change to minus when expanded + details[open] & { + &::before { + content: '−'; + } + } + + &:hover::before { + color: $article-link-hover; + border-color: $article-link-hover; + } + } + + // Quick Reference expandable section + .quick-reference { + margin-top: 2rem; + + details { + border: 1px solid $article-hr; + border-radius: var(--border-radius); + padding: 0.5rem; + } + + .reference-table { + margin-top: 1rem; + width: 100%; + border-collapse: collapse; + font-size: 0.9em; + + th, td { + padding: 0.5rem; + text-align: left; + border: 1px solid $article-hr; + } + + th { + padding: 0.75rem 0.5rem; + background: rgba($article-link, 0.1); + font-weight: 600; + } + + tbody tr:nth-child(even) { + background: rgba($article-text, 0.02); + } + + .product-name { + font-weight: 600; + } + } + } + +} \ No newline at end of file diff --git a/assets/styles/layouts/_modals.scss b/assets/styles/layouts/_modals.scss index 5a46d5b6c..2a149c378 100644 --- a/assets/styles/layouts/_modals.scss +++ b/assets/styles/layouts/_modals.scss @@ -26,7 +26,8 @@ .modal-body { position: relative; display: flex; - overflow: hidden; + overflow-y: auto; + overflow-x: hidden; // width: 100%; max-width: 650px; max-height: 97.5vh; @@ -37,6 +38,27 @@ color: $article-text; font-size: 1rem; transition: margin .4s; + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; // iOS smooth scrolling + + // Custom scrollbar styling + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: rgba($article-hr, 0.2); + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb { + background: rgba($article-text, 0.3); + border-radius: 4px; + + &:hover { + background: rgba($article-text, 0.5); + } + } } &.open { @@ -62,6 +84,7 @@ overflow: visible; width: 586px; max-width: 100%; + flex-shrink: 0; h3 { color: $article-heading; diff --git a/assets/styles/styles-default.scss b/assets/styles/styles-default.scss index 5fd3eed2d..6f873fb20 100644 --- a/assets/styles/styles-default.scss +++ b/assets/styles/styles-default.scss @@ -34,3 +34,6 @@ "layouts/code-controls", "layouts/v3-wayfinding"; +// Import Components +@import "components/influxdb-version-detector"; + diff --git a/content/shared/influxdb3-visualize/grafana.md b/content/shared/influxdb3-visualize/grafana.md index 06dbf82bc..50bf89c48 100644 --- a/content/shared/influxdb3-visualize/grafana.md +++ b/content/shared/influxdb3-visualize/grafana.md @@ -1,4 +1,4 @@ -Use [Grafana](https://grafana.com/) to query and visualize data from +Use [Grafana](https://grafana.com/) to query and visualize data from {{% product-name %}}. > [Grafana] enables you to query, visualize, alert on, and explore your metrics, @@ -8,6 +8,10 @@ Use [Grafana](https://grafana.com/) to query and visualize data from > > {{% cite %}}-- [Grafana documentation](https://grafana.com/docs/grafana/latest/introduction/){{% /cite %}} + +> [!Note] +> {{< influxdb-version-detector >}} + - [Install Grafana or login to Grafana Cloud](#install-grafana-or-login-to-grafana-cloud) - [InfluxDB data source](#influxdb-data-source) - [Create an InfluxDB data source](#create-an-influxdb-data-source) diff --git a/content/test-version-detector.md b/content/test-version-detector.md new file mode 100644 index 000000000..452c00872 --- /dev/null +++ b/content/test-version-detector.md @@ -0,0 +1,18 @@ +--- +title: Test InfluxDB Version Detector +description: Test page for the InfluxDB version detector component +weight: 1000 +test_only: true # Custom parameter to indicate test-only content +--- + +This is a test page for the InfluxDB version detector component. + +{{< influxdb-version-detector >}} + +## About this component + +This interactive component helps users identify which InfluxDB product they're using by: +- Checking URL patterns +- Analyzing ping response headers +- Asking guided questions about their setup +- Providing ranked results based on their answers \ No newline at end of file diff --git a/cypress/e2e/content/influxdb-version-detector.cy.js b/cypress/e2e/content/influxdb-version-detector.cy.js new file mode 100644 index 000000000..937dea677 --- /dev/null +++ b/cypress/e2e/content/influxdb-version-detector.cy.js @@ -0,0 +1,861 @@ +/// + +/** + * InfluxDB Version Detector E2E Test Suite + * + * COMPREHENSIVE TEST SCENARIOS CHECKLIST: + * + * URL Detection Scenarios: + * ------------------------- + * Cloud URLs (Definitive Detection): + * - [ ] Dedicated: https://cluster-id.influxdb.io → InfluxDB Cloud Dedicated (confidence 1.0) + * - [ ] Serverless US: https://us-east-1-1.aws.cloud2.influxdata.com → InfluxDB Cloud Serverless + * - [ ] Serverless EU: https://eu-central-1-1.aws.cloud2.influxdata.com → InfluxDB Cloud Serverless + * - [ ] Cloud TSM: https://us-west-2-1.aws.cloud2.influxdata.com → InfluxDB Cloud v2 (TSM) + * - [ ] Cloud v1: https://us-west-1-1.influxcloud.net → InfluxDB Cloud v1 + * + * Localhost URLs (Port-based Detection): + * - [ ] Core/Enterprise Port: http://localhost:8181 → Should suggest ping test + * - [ ] OSS Port: http://localhost:8086 → Should suggest version check + * - [ ] Custom Port: http://localhost:9999 → Should fall back to questionnaire + * + * Edge Cases: + * - [ ] Empty URL: Submit without entering URL → Should show error + * - [ ] Invalid URL: "not-a-url" → Should fall back to questionnaire + * - [ ] Cloud keyword: "cloud 2" → Should start questionnaire with cloud context + * - [ ] Mixed case: HTTP://LOCALHOST:8181 → Should detect port correctly + * + * Airgapped/Manual Analysis Scenarios: + * ------------------------------------- + * Ping Headers Analysis: + * - [ ] v3 Core headers: x-influxdb-build: core → InfluxDB 3 Core + * - [ ] v3 Enterprise headers: x-influxdb-build: enterprise → InfluxDB 3 Enterprise + * - [ ] v2 OSS headers: X-Influxdb-Version: 2.7.8 → InfluxDB OSS 2.x + * - [ ] v1 headers: X-Influxdb-Version: 1.8.10 → InfluxDB OSS 1.x + * - [ ] 401 Response: Headers showing 401/403 → Should show auth required message + * - [ ] Empty headers: Submit without text → Should show error + * - [ ] Example content: Submit with placeholder text → Should show error + * + * Docker Output Analysis: + * - [ ] Explicit v3 Core: "InfluxDB 3 Core" in output → InfluxDB 3 Core + * - [ ] Explicit v3 Enterprise: "InfluxDB 3 Enterprise" in output → InfluxDB 3 Enterprise + * - [ ] Generic v3: x-influxdb-version: 3.1.0 but no build header → Core or Enterprise + * - [ ] v2 version: "InfluxDB v2.7.8" in output → InfluxDB OSS 2.x + * - [ ] v1 OSS: "InfluxDB v1.8.10" in output → InfluxDB OSS 1.x + * - [ ] v1 Enterprise: "InfluxDB 1.8.10" + "Enterprise" → InfluxDB Enterprise + * - [ ] Empty output: Submit without text → Should show error + * - [ ] Example content: Submit with placeholder text → Should show error + * + * Questionnaire Flow Scenarios: + * ------------------------------ + * License-based Paths: + * - [ ] Free → Self-hosted → Recent → SQL → Should rank Core/OSS highly + * - [ ] Paid → Self-hosted → Recent → SQL → Should rank Enterprise highly + * - [ ] Free → Cloud → Recent → Flux → Should rank Cloud Serverless/TSM + * - [ ] Paid → Cloud → Recent → SQL → Should rank Dedicated highly + * - [ ] Unknown license → Should not eliminate products + * + * Age-based Scoring: + * - [ ] Recent (< 1 year) → Should favor v3 products + * - [ ] 1-5 years → Should favor v2 era products + * - [ ] 5+ years → Should favor v1 products only + * - [ ] Unknown age → Should not affect scoring + * + * Language-based Elimination: + * - [ ] SQL only → Should eliminate v1, v2, Cloud TSM + * - [ ] Flux only → Should eliminate v1, all v3 products + * - [ ] InfluxQL only → Should favor v1, but not eliminate others + * - [ ] Multiple languages → Should not eliminate products + * - [ ] Unknown language → Should not affect scoring + * + * Combined Detection Scenarios: + * ----------------------------- + * URL + Questionnaire: + * - [ ] Port 8181 + Free license → Should show Core as high confidence + * - [ ] Port 8181 + Paid license → Should show Enterprise as high confidence + * - [ ] Port 8086 + Free + Recent + SQL → Mixed signals, show ranked results + * - [ ] Cloud URL pattern + Paid → Should favor Dedicated/Serverless + * + * UI/UX Scenarios: + * ---------------- + * Navigation: + * - [ ] Back button: From URL input → Should return to "URL known" question + * - [ ] Back button: From questionnaire Q2 → Should return to Q1 + * - [ ] Back button: From first question → Should stay at first question + * - [ ] Progress bar: Should update with each question + * + * Results Display: + * - [ ] High confidence (score > 60): Should show "Most Likely" label + * - [ ] Medium confidence (30-60): Should show confidence rating + * - [ ] Low confidence (< 30): Should show multiple candidates + * - [ ] Score gap ≥ 15: Top result should stand out + * - [ ] Score gap < 15: Should show multiple options + * + * Interactive Elements: + * - [ ] Start questionnaire button: From detection results → Should hide results and start questions + * - [ ] Restart button: Should clear all answers and return to start + * - [ ] Grafana links: Should display for detected products + * - [ ] Configuration guidance: Should display for top results + * - [ ] Quick reference table: Should expand/collapse + * + * Pre-filled Values: + * - [ ] Stored URL: Should pre-fill URL input from localStorage + * - [ ] URL indicator: Should show when URL is pre-filled + * - [ ] Clear indicator: Should hide when user edits URL + * + * Analytics Tracking Scenarios: + * ----------------------------- + * - [ ] Modal opened: Track when component initializes + * - [ ] Question answered: Track each answer with question_id and value + * - [ ] URL detection: Track with detection_method: "url_analysis" + * - [ ] Product detected: Track with detected_product and completion_status + * - [ ] Restart: Track restart action + * + * Accessibility Scenarios: + * ------------------------ + * - [ ] Keyboard navigation: Tab through buttons and inputs + * - [ ] Focus management: Should focus on heading after showing result + * - [ ] Screen reader: Labels and ARIA attributes present + * - [ ] Color contrast: Results visible in different themes + * + * Error Handling: + * --------------- + * - [ ] Missing products data: Component should handle gracefully + * - [ ] Missing influxdb_urls data: Should use fallback values + * - [ ] Invalid JSON in data attributes: Should log warning and continue + * + * Edge Cases: + * ----------- + * - [ ] Modal initialization: Component in modal should wait for modal to open + * - [ ] Multiple instances: Each instance should work independently + * - [ ] Page navigation: State should persist if using back button + * - [ ] URL query params: Should update with detection results + */ + +const modalTriggerSelector = 'a.btn.influxdb-detector-trigger'; + +describe('InfluxDB Version Detector Component', function () { + // Remove the global beforeEach to optimize for efficient running + // Each describe block will visit the page once + + describe('Component Data Attributes', function () { + beforeEach(() => { + cy.visit('/test-version-detector/'); + // The trigger is an anchor element with .btn class, not a button + cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + }); + + it('should not throw JavaScript console errors', function () { + cy.window().then((win) => { + const logs = []; + const originalError = win.console.error; + + win.console.error = (...args) => { + logs.push(args.join(' ')); + originalError.apply(win.console, args); + }; + + cy.wait(2000); + + cy.then(() => { + const relevantErrors = logs.filter( + (log) => + log.includes('influxdb-version-detector') || + log.includes('detectContext is not a function') || + log.includes('Failed to parse influxdb_urls data') + ); + expect(relevantErrors).to.have.length(0); + }); + }); + }); + }); + + describe('URL with port 8086', function () { + before(() => { + cy.visit('/test-version-detector/'); + cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + cy.get('[data-component="influxdb-version-detector"]') + .eq(0) + .within(() => { + cy.get('.option-button').contains('Yes, I know the URL').click(); + it('should suggest legacy editions for custom URL or hostname', function () { + cy.get('#url-input', { timeout: 10000 }) + .clear() + .type('http://willieshotchicken.com:8086'); + cy.get('.submit-button').click(); + + cy.get('.result') + .invoke('text') + .then((text) => { + // Should mention multiple products for port 8086 + const mentionsLegacyEditions = + text.includes('OSS 1.x') || + text.includes('OSS 2.x') || + text.includes('Enterprise'); + expect(mentionsLegacyEditions).to.be.true; + }); + cy.get('#url-input', { timeout: 10000 }) + .should('be.visible') + .clear() + .type('willieshotchicken.com:8086'); + cy.get('.submit-button').click(); + + cy.get('.result') + .invoke('text') + .then((text) => { + // Should mention multiple products + const mentionsLegacyEditions = + text.includes('OSS 1.x') || + text.includes('OSS 2.x') || + text.includes('Enterprise'); + expect(mentionsLegacyEditions).to.be.true; + }); + }); + it('should suggest OSS for localhost', function () { + cy.get('#url-input', { timeout: 10000 }) + .should('be.visible') + .clear() + .type('http://localhost:8086'); + cy.get('.submit-button').click(); + + cy.get('.result') + .invoke('text') + .then((text) => { + // Should mention multiple editions + const mentionsLegacyOSS = + text.includes('OSS 1.x') || text.includes('OSS 2.x'); + expect(mentionsLegacyOSS).to.be.true; + }); + }); + }); + }); + + describe.skip('URL with port 8181', function () { + const port8181UrlTests = [ + // InfluxDB 3 Core/Enterprise URLs + { + url: 'http://localhost:8181', + }, + { + url: 'https://my-server.com:8181', + }, + ]; + port8181UrlTests.forEach(({ url }) => { + it(`should detect Core and Enterprise 3 for ${url}`, function () { + cy.visit('/test-version-detector/'); + cy.get('body').then(($body) => { + cy.get('#influxdb-url').clear().type(url); + cy.get('.submit-button').click(); + cy.get('.result') + .invoke('text') + .then((text) => { + // Should mention multiple editions + const mentionsCoreAndEnterprise = + text.includes('InfluxDB 3 Core') && + text.includes('InfluxDB 3 Enterprise'); + expect(mentionsCoreAndEnterprise).to.be.true; + }); + }); + }); + }); + }); + + describe.skip('Cloud URLs', function () { + const cloudUrlTests = [ + { + url: 'https://us-west-2-1.aws.cloud2.influxdata.com', + expectedText: 'Cloud', + }, + { + url: 'https://us-east-1-1.aws.cloud2.influxdata.com', + expectedText: 'Cloud', + }, + { + url: 'https://eu-central-1-1.aws.cloud2.influxdata.com', + expectedText: 'Cloud', + }, + { + url: 'https://us-central1-1.gcp.cloud2.influxdata.com', + expectedText: 'Cloud', + }, + { + url: 'https://westeurope-1.azure.cloud2.influxdata.com', + expectedText: 'Cloud', + }, + { + url: 'https://eastus-1.azure.cloud2.influxdata.com', + expectedText: 'Cloud', + }, + ]; + + cloudUrlTests.forEach(({ url, expectedText }) => { + it(`should detect ${expectedText} for ${url}`, function () { + cy.visit('/test-version-detector/'); + cy.get('body').then(($body) => { + cy.get('#influxdb-url').clear().type(url); + cy.get('.submit-button').click(); + + cy.get('.result').should('be.visible').and('contain', expectedText); + }); + }); + }); + }); + + describe.skip('Cloud Dedicated and Clustered URLs', function () { + const clusterUrlTests = [ + // v3 Cloud Dedicated + { + url: 'https://cluster-id.a.influxdb.io', + expectedText: 'Cloud Dedicated', + }, + { + url: 'https://my-cluster.a.influxdb.io', + expectedText: 'Cloud Dedicated', + }, + + // v1 Enterprise/v3 Clustered + { url: 'https://cluster-host.com', expectedText: 'Clustered' }, + ]; + clusterUrlTests.forEach(({ url, expectedText }) => { + it(`should detect ${expectedText} for ${url}`, function () { + cy.visit('/test-version-detector/'); + cy.get('body').then(($body) => { + cy.get('#influxdb-url').clear().type(url); + cy.get('.submit-button').click(); + + cy.get('.result').should('be.visible').and('contain', expectedText); + }); + }); + }); + }); + + describe.skip('Cloud Dedicated and Clustered URLs', function () { + const clusterUrlTests = [ + // v3 Cloud Dedicated + { + url: 'https://cluster-id.a.influxdb.io', + expectedText: 'Cloud Dedicated', + }, + { + url: 'https://my-cluster.a.influxdb.io', + expectedText: 'Cloud Dedicated', + }, + + // v1 Enterprise/v3 Clustered + { url: 'https://cluster-host.com', expectedText: 'Clustered' }, + ]; + clusterUrlTests.forEach(({ url, expectedText }) => { + it(`should detect ${expectedText} for ${url}`, function () { + cy.visit('/test-version-detector/'); + cy.get('body').then(($body) => { + cy.get('#influxdb-url').clear().type(url); + cy.get('.submit-button').click(); + + cy.get('.result').should('be.visible').and('contain', expectedText); + }); + }); + }); + }); + + it('should handle cloud context detection', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + // Wait for URL input question to appear and then enter cloud context + cy.get('#q-url-input', { timeout: 10000 }).should('be.visible'); + cy.get('#url-input', { timeout: 10000 }) + .should('be.visible') + .clear() + .type('cloud 2'); + cy.get('.submit-button').click(); + + // Should proceed to next step - either show result or start questionnaire + // Don't be too specific about what happens next, just verify it progresses + cy.get('body').then(($body) => { + if ($body.find('.result').length > 0) { + cy.get('.result').should('be.visible'); + } else { + cy.get('.question.active', { timeout: 15000 }).should('be.visible'); + } + }); + }); + + it('should handle v3 port detection', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + // Wait for URL input question to appear and then test v3 port detection (8181) + cy.get('#q-url-input', { timeout: 10000 }).should('be.visible'); + cy.get('#url-input', { timeout: 10000 }) + .should('be.visible') + .clear() + .type('http://localhost:8181'); + cy.get('.submit-button').click(); + + // Should progress to either result or questionnaire + cy.get('body', { timeout: 15000 }).then(($body) => { + if ($body.find('.result').length > 0) { + cy.get('.result').should('be.visible'); + } else { + cy.get('.question.active').should('be.visible'); + } + }); + }); + }); + + describe.skip('Questionnaire Flow', function () { + beforeEach(() => { + cy.visit('/test-version-detector/'); + // The trigger is an anchor element with .btn class, not a button + cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + }); + it('should start questionnaire for unknown URL', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + cy.get('#url-input').clear().type('https://unknown-server.com:9999'); + cy.get('.submit-button').click(); + + cy.get('.question.active').should('be.visible'); + cy.get('.option-button').should('have.length.greaterThan', 0); + }); + + it('should complete basic questionnaire flow', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + // Start questionnaire + cy.get('#url-input') + .should('be.visible') + .clear() + .type('https://test.com'); + cy.get('.submit-button').click(); + cy.get('.question.active', { timeout: 10000 }).should('be.visible'); + + // Answer questions with proper waiting for DOM updates + const answers = ['Self-hosted', 'Free', '2-5 years', 'SQL']; + + answers.forEach((answer, index) => { + cy.get('.question.active', { timeout: 10000 }).should('be.visible'); + cy.get('.back-button').should('be.visible'); + cy.get('.option-button').contains(answer).should('be.visible').click(); + + // Wait for the next question or final result + if (index < answers.length - 1) { + cy.get('.question.active', { timeout: 5000 }).should('be.visible'); + } + }); + + // Should show results + cy.get('.result', { timeout: 10000 }).should('be.visible'); + }); + + it('should show all products when answering "I\'m not sure" to all questions', function () { + // Test fix for: Core/Enterprise disappearing with all "unknown" answers + cy.get('.option-button').contains("No, I don't know the URL").click(); + cy.get('.question.active', { timeout: 10000 }).should('be.visible'); + + // Answer "I'm not sure" to all questions + for (let i = 0; i < 4; i++) { + cy.get('.question.active', { timeout: 10000 }).should('be.visible'); + cy.get('.option-button').contains("I'm not sure").click(); + cy.wait(500); + } + + cy.get('.result', { timeout: 10000 }).should('be.visible'); + // Should show multiple products, not empty or filtered list + cy.get('.result').invoke('text').should('have.length.greaterThan', 100); + }); + + it('should NOT recommend InfluxDB 3 for Flux users (regression test)', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + cy.get('#url-input').should('be.visible').clear().type('cloud 2'); + cy.get('.submit-button').click(); + cy.get('.question.active', { timeout: 10000 }).should('be.visible'); + + // Complete problematic scenario that was fixed + const answers = ['Paid', '2-5 years', 'Flux']; + answers.forEach((answer, index) => { + cy.get('.question.active', { timeout: 10000 }).should('be.visible'); + cy.get('.option-button').contains(answer).should('be.visible').click(); + + // Wait for the next question or final result + if (index < answers.length - 1) { + cy.get('.question.active', { timeout: 5000 }).should('be.visible'); + } + }); + + cy.get('.result', { timeout: 10000 }).should('be.visible'); + + // Should NOT recommend InfluxDB 3 products for Flux + cy.get('.result').should('not.contain', 'InfluxDB 3 Core'); + cy.get('.result').should('not.contain', 'InfluxDB 3 Enterprise'); + }); + + // Comprehensive questionnaire scenarios covering all decision tree paths + const questionnaireScenarios = [ + { + name: 'SQL Filtering Test - Only InfluxDB 3 products for SQL (Free)', + answers: ['Self-hosted', 'Free', 'Less than 6 months', 'SQL'], + shouldContain: ['InfluxDB 3'], + shouldNotContain: [ + 'InfluxDB OSS 1.x', + 'InfluxDB OSS 2.x', + 'InfluxDB Enterprise v1.x', + 'InfluxDB Cloud (TSM)', + ], + }, + { + name: 'SQL Filtering Test - Only InfluxDB 3 products for SQL (Paid)', + answers: ['Self-hosted', 'Paid', 'Less than 6 months', 'SQL'], + shouldContain: ['InfluxDB 3'], + shouldNotContain: [ + 'InfluxDB OSS 1.x', + 'InfluxDB OSS 2.x', + 'InfluxDB Enterprise v1.x', + 'InfluxDB Cloud (TSM)', + ], + }, + { + name: 'SQL Filtering Test - Only InfluxDB 3 Cloud products for SQL', + answers: [ + 'Cloud (managed service)', + 'Paid', + 'Less than 6 months', + 'SQL', + ], + shouldContain: ['Cloud'], + shouldNotContain: [ + 'InfluxDB OSS', + 'InfluxDB Enterprise v1.x', + 'InfluxDB Cloud (TSM)', + ], + }, + { + name: 'OSS Free User - SQL (recent)', + answers: ['Self-hosted', 'Free', 'Less than 6 months', 'SQL'], + shouldContain: ['InfluxDB 3 Core'], + shouldNotContain: ['InfluxDB 3 Enterprise'], + }, + { + name: 'OSS Free User - SQL (experienced)', + answers: ['Self-hosted', 'Free', '2-5 years', 'SQL'], + shouldContain: ['InfluxDB 3 Core'], + shouldNotContain: ['InfluxDB 3 Enterprise'], + }, + { + name: 'Cloud Flux User', + answers: [ + 'Cloud (managed service)', + 'Paid', + 'Less than 6 months', + 'Flux', + ], + shouldContain: ['InfluxDB v2', 'Cloud'], + shouldNotContain: ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'], + }, + { + name: 'Cloud SQL User (recent)', + answers: [ + 'Cloud (managed service)', + 'Paid', + 'Less than 6 months', + 'SQL', + ], + shouldContain: ['Cloud Serverless', 'Cloud Dedicated'], + shouldNotContain: ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'], + }, + { + name: 'Modern Self-hosted SQL User', + answers: ['Self-hosted', 'Paid', 'Less than 6 months', 'SQL'], + shouldContain: ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'], + shouldNotContain: ['Cloud'], + }, + { + name: 'High Volume Enterprise User', + answers: ['Self-hosted', 'Paid', 'Less than 6 months', 'SQL', 'Yes'], + shouldContain: ['InfluxDB 3 Enterprise'], + shouldNotContain: ['Cloud'], + }, + { + name: 'Legacy Self-hosted User (InfluxQL)', + answers: ['Self-hosted', 'Free', '5+ years', 'InfluxQL'], + shouldContain: ['InfluxDB v1', 'OSS'], + shouldNotContain: ['InfluxDB 3'], + }, + { + name: 'Legacy Enterprise User', + answers: ['Self-hosted', 'Paid', '5+ years', 'InfluxQL'], + shouldContain: ['InfluxDB Enterprise', 'InfluxDB v1'], + shouldNotContain: ['InfluxDB 3'], + }, + { + name: 'Experienced OSS User (Flux)', + answers: ['Self-hosted', 'Free', '2-5 years', 'Flux'], + shouldContain: ['InfluxDB v2', 'OSS'], + shouldNotContain: ['InfluxDB 3', 'Enterprise'], + }, + { + name: 'Cloud Free User (recent)', + answers: [ + 'Cloud (managed service)', + 'Free', + 'Less than 6 months', + 'SQL', + ], + shouldContain: ['Cloud Serverless'], + shouldNotContain: ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'], + }, + { + name: 'SQL Cloud User - Only InfluxDB 3 Cloud products', + answers: [ + 'Cloud (managed service)', + 'Paid', + 'Less than 6 months', + 'SQL', + ], + shouldContain: ['Cloud Serverless', 'Cloud Dedicated'], + shouldNotContain: [ + 'InfluxDB OSS', + 'InfluxDB Enterprise v1', + 'InfluxDB Cloud (TSM)', + ], + }, + { + name: 'Uncertain User', + answers: [ + "I'm not sure", + "I'm not sure", + "I'm not sure", + "I'm not sure", + ], + shouldContain: [], // Should still provide some recommendations + shouldNotContain: [], + }, + ]; + + questionnaireScenarios.forEach((scenario) => { + it(`should handle questionnaire scenario: ${scenario.name}`, function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + // Start questionnaire + cy.get('#url-input').clear().type('https://unknown-server.com:9999'); + cy.get('.submit-button').click(); + cy.get('.question.active').should('be.visible'); + + // Answer questions + scenario.answers.forEach((answer) => { + cy.get('.question.active').should('be.visible'); + cy.get('.option-button').contains(answer).click(); + cy.wait(500); + }); + + // Verify results + cy.get('.result').should('be.visible'); + + // Check expected content + scenario.shouldContain.forEach((product) => { + cy.get('.result').should('contain', product); + }); + + // Check content that should NOT be present + scenario.shouldNotContain.forEach((product) => { + cy.get('.result').should('not.contain', product); + }); + }); + + it('should NOT recommend InfluxDB 3 for 5+ year installations (time-aware)', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + cy.get('#url-input').clear().type('https://unknown-server.com:9999'); + cy.get('.submit-button').click(); + cy.get('.question.active').should('be.visible'); + + // Test that v3 products are excluded for 5+ years + const answers = ['Free', 'Self-hosted', 'More than 5 years', 'SQL']; + answers.forEach((answer) => { + cy.get('.question.active').should('be.visible'); + cy.get('.option-button').contains(answer).click(); + cy.wait(500); + }); + + cy.get('.result').should('be.visible'); + cy.get('.result').should('not.contain', 'InfluxDB 3 Core'); + cy.get('.result').should('not.contain', 'InfluxDB 3 Enterprise'); + // Should recommend legacy products instead + cy.get('.result').should('contain', 'InfluxDB'); + }); + }); + + it('should apply -100 Flux penalty to InfluxDB 3 products', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + cy.get('#url-input').clear().type('https://unknown-server.com:9999'); + cy.get('.submit-button').click(); + cy.get('.question.active').should('be.visible'); + + // Even for recent, paid, self-hosted users, Flux should eliminate v3 products + const answers = ['Self-hosted', 'Paid', 'Less than 6 months', 'Flux']; + answers.forEach((answer) => { + cy.get('.question.active').should('be.visible'); + cy.get('.option-button').contains(answer).click(); + cy.wait(500); + }); + + cy.get('.result').should('be.visible'); + cy.get('.result').should('not.contain', 'InfluxDB 3 Core'); + cy.get('.result').should('not.contain', 'InfluxDB 3 Enterprise'); + }); + + it('should detect cloud context correctly with regex patterns', function () { + const cloudPatterns = ['cloud 2', 'cloud v2', 'influxdb cloud 2']; + + // Test first pattern in current session + cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#url-input').clear().type(cloudPatterns[0]); + cy.get('.submit-button').click(); + cy.get('.question.active').should('be.visible'); + }); + + // Navigation and interaction tests + it('should allow going back through questionnaire questions', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + // Start questionnaire + cy.get('#url-input').clear().type('https://unknown-server.com:9999'); + cy.get('.submit-button').click(); + cy.get('.question.active').should('be.visible'); + + // Answer first question + cy.get('.option-button').first().click(); + cy.wait(500); + + // Check if back button exists and is clickable + cy.get('body').then(($body) => { + if ($body.find('.back-button').length > 0) { + cy.get('.back-button').should('be.visible').click(); + cy.get('.question.active').should('be.visible'); + } + }); + }); + + it('should allow restarting questionnaire from results', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + // Complete a questionnaire + cy.get('#url-input').clear().type('https://unknown-server.com:9999'); + cy.get('.submit-button').click(); + cy.get('.question.active').should('be.visible'); + + const answers = ['Self-hosted', 'Free', '2-5 years', 'SQL']; + answers.forEach((answer) => { + cy.get('.question.active').should('be.visible'); + cy.get('.back-button').should('be.visible'); + cy.get('.option-button').contains(answer).click(); + cy.wait(500); + }); + + cy.get('.result').should('be.visible'); + + // Check if restart button exists and works + cy.get('body').then(($body) => { + if ($body.find('.restart-button').length > 0) { + cy.get('.restart-button').should('be.visible').click(); + cy.get('.question.active').should('be.visible'); + } + }); + }); + }); + + describe.skip('Basic Error Handling', function () { + beforeEach(() => { + cy.visit('/test-version-detector/'); + // The trigger is an anchor element with .btn class, not a button + cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + }); + + it('should handle empty URL input gracefully', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + cy.get('#url-input').clear(); + cy.get('.submit-button').click(); + + // Should start questionnaire or show guidance + cy.get('.question.active, .result').should('be.visible'); + }); + + it('should handle invalid URL format gracefully', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + cy.get('#url-input').clear().type('not-a-valid-url'); + cy.get('.submit-button').click(); + + // Should handle gracefully + cy.get('.question.active, .result').should('be.visible'); + }); + }); + + describe.skip('SQL Language Filtering', function () { + beforeEach(() => { + cy.visit('/test-version-detector/'); + // The trigger is an anchor element with .btn class, not a button + cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + }); + + it('should only show InfluxDB 3 products when SQL is selected', function () { + // Click "Yes, I know the URL" first + cy.get('.option-button').contains('Yes, I know the URL').click(); + + // Start questionnaire with unknown URL + cy.get('#url-input').clear().type('https://unknown-server.com:9999'); + cy.get('.submit-button').click(); + cy.get('.question.active').should('be.visible'); + + // Answer questions leading to SQL selection + const answers = ['Self-hosted', 'Free', 'Less than 6 months', 'SQL']; + answers.forEach((answer) => { + cy.get('.question.active').should('be.visible'); + cy.get('.option-button').contains(answer).click(); + cy.wait(500); + }); + + cy.get('.result').should('be.visible'); + + // Get the full result text to verify filtering + cy.get('.result') + .invoke('text') + .then((resultText) => { + // Verify that ONLY InfluxDB 3 products are shown + const shouldNotContain = [ + 'InfluxDB Enterprise v1.x', + 'InfluxDB OSS v2.x', + 'InfluxDB OSS 1.x', + 'InfluxDB Cloud (TSM)', + ]; + + // Check that forbidden products are NOT in results + shouldNotContain.forEach((forbiddenProduct) => { + expect(resultText).to.not.contain(forbiddenProduct); + }); + + // Verify at least one InfluxDB 3 product is shown + const hasValidProduct = + resultText.includes('InfluxDB 3 Core') || + resultText.includes('Cloud Dedicated') || + resultText.includes('Cloud Serverless') || + resultText.includes('InfluxDB Clustered'); + + expect(hasValidProduct).to.be.true; + }); + }); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 66ea16ef0..96e2b2f3c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -22,4 +22,7 @@ // // // -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) \ No newline at end of file +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + +// Import custom commands for InfluxDB Version Detector +import './influxdb-version-detector-commands.js'; diff --git a/cypress/support/influxdb-version-detector-commands.js b/cypress/support/influxdb-version-detector-commands.js new file mode 100644 index 000000000..4cb0ebde8 --- /dev/null +++ b/cypress/support/influxdb-version-detector-commands.js @@ -0,0 +1,299 @@ +// Custom Cypress commands for InfluxDB Version Detector testing + +/** + * Navigate to a page with the version detector component + * @param {string} [path='/influxdb3/core/visualize-data/grafana/'] - Path to a page with the component + */ +Cypress.Commands.add( + 'visitVersionDetector', + (path = '/influxdb3/core/visualize-data/grafana/') => { + cy.visit(path); + cy.get('[data-component="influxdb-version-detector"]', { + timeout: 10000, + }).should('be.visible'); + } +); + +/** + * Test URL detection for a specific URL + * @param {string} url - The URL to test + * @param {string} expectedProduct - Expected product name in the result + */ +Cypress.Commands.add('testUrlDetection', (url, expectedProduct) => { + cy.get('#influxdb-url').clear().type(url); + cy.get('.submit-button').click(); + + cy.get('.result.show', { timeout: 5000 }).should('be.visible'); + cy.get('.detected-version').should('contain', expectedProduct); +}); + +/** + * Complete a questionnaire with given answers + * @param {string[]} answers - Array of answers to select in order + */ +Cypress.Commands.add('completeQuestionnaire', (answers) => { + answers.forEach((answer, index) => { + cy.get('.question.active', { timeout: 3000 }).should('be.visible'); + cy.get('.option-button').contains(answer).should('be.visible').click(); + + // Wait for transition between questions + if (index < answers.length - 1) { + cy.wait(500); + } + }); + + // Wait for final results + cy.get('.result.show', { timeout: 5000 }).should('be.visible'); +}); + +/** + * Start questionnaire with unknown URL + * @param {string} [url='https://unknown-server.com:9999'] - URL to trigger questionnaire + */ +Cypress.Commands.add( + 'startQuestionnaire', + (url = 'https://unknown-server.com:9999') => { + cy.get('#influxdb-url').clear().type(url); + cy.get('.submit-button').click(); + cy.get('.question.active', { timeout: 5000 }).should('be.visible'); + } +); + +/** + * Verify questionnaire results contain/don't contain specific products + * @param {Object} options - Configuration object + * @param {string[]} [options.shouldContain] - Products that should be in results + * @param {string[]} [options.shouldNotContain] - Products that should NOT be in results + */ +Cypress.Commands.add( + 'verifyQuestionnaireResults', + ({ shouldContain = [], shouldNotContain = [] }) => { + cy.get('.result.show', { timeout: 5000 }).should('be.visible'); + + shouldContain.forEach((product) => { + cy.get('.result').should('contain', product); + }); + + shouldNotContain.forEach((product) => { + cy.get('.result').should('not.contain', product); + }); + } +); + +/** + * Test navigation through questionnaire (back/restart functionality) + */ +Cypress.Commands.add('testQuestionnaireNavigation', () => { + // Answer first question + cy.get('.option-button').first().click(); + cy.wait(500); + + // Test back button + cy.get('.back-button').should('be.visible').click(); + cy.get('.question.active').should('be.visible'); + cy.get('.progress .progress-bar').should('have.css', 'width', '0px'); + + // Complete questionnaire to test restart + const quickAnswers = ['Self-hosted', 'Free', '2-5 years', 'SQL']; + cy.completeQuestionnaire(quickAnswers); + + // Test restart button + cy.get('.restart-button', { timeout: 3000 }).should('be.visible').click(); + cy.get('.question.active').should('be.visible'); + cy.get('.progress .progress-bar').should('have.css', 'width', '0px'); +}); + +/** + * Check for JavaScript console errors related to the component + */ +Cypress.Commands.add('checkForConsoleErrors', () => { + cy.window().then((win) => { + const logs = []; + const originalConsoleError = win.console.error; + + win.console.error = (...args) => { + logs.push(args.join(' ')); + originalConsoleError.apply(win.console, args); + }; + + // Wait for any potential errors to surface + cy.wait(1000); + + cy.then(() => { + const relevantErrors = logs.filter( + (log) => + log.includes('influxdb-version-detector') || + log.includes('Failed to parse influxdb_urls data') || + log.includes('SyntaxError') || + log.includes('#ZgotmplZ') || + log.includes('detectContext is not a function') + ); + + if (relevantErrors.length > 0) { + throw new Error( + `Console errors detected: ${relevantErrors.join('; ')}` + ); + } + }); + }); +}); + +/** + * Test URL scenarios from the influxdb_urls.yml data + */ +Cypress.Commands.add('testAllKnownUrls', () => { + const urlTestCases = [ + // OSS URLs + { url: 'http://localhost:8086', product: 'InfluxDB OSS' }, + { url: 'https://my-server.com:8086', product: 'InfluxDB OSS' }, + + // InfluxDB 3 URLs + { url: 'http://localhost:8181', product: 'InfluxDB 3' }, + { url: 'https://my-server.com:8181', product: 'InfluxDB 3' }, + + // Cloud URLs + { + url: 'https://us-west-2-1.aws.cloud2.influxdata.com', + product: 'InfluxDB Cloud', + }, + { + url: 'https://us-east-1-1.aws.cloud2.influxdata.com', + product: 'InfluxDB Cloud', + }, + { + url: 'https://eu-central-1-1.aws.cloud2.influxdata.com', + product: 'InfluxDB Cloud', + }, + { + url: 'https://us-central1-1.gcp.cloud2.influxdata.com', + product: 'InfluxDB Cloud', + }, + { + url: 'https://westeurope-1.azure.cloud2.influxdata.com', + product: 'InfluxDB Cloud', + }, + { + url: 'https://eastus-1.azure.cloud2.influxdata.com', + product: 'InfluxDB Cloud', + }, + + // Cloud Dedicated + { + url: 'https://cluster-id.a.influxdb.io', + product: 'InfluxDB Cloud Dedicated', + }, + { + url: 'https://my-cluster.a.influxdb.io', + product: 'InfluxDB Cloud Dedicated', + }, + + // Clustered + { url: 'https://cluster-host.com', product: 'InfluxDB Clustered' }, + ]; + + urlTestCases.forEach(({ url, product }) => { + cy.visitVersionDetector(); + cy.testUrlDetection(url, product); + }); +}); + +/** + * Test comprehensive questionnaire scenarios + */ +Cypress.Commands.add('testQuestionnaireScenarios', () => { + const scenarios = [ + { + name: 'OSS Free User', + answers: ['Self-hosted', 'Free', '2-5 years', 'SQL'], + shouldContain: ['InfluxDB OSS', 'InfluxDB v2'], + shouldNotContain: ['InfluxDB 3 Enterprise'], + }, + { + name: 'Cloud Flux User', + answers: [ + 'Cloud (managed service)', + 'Paid', + 'Less than 6 months', + 'Flux', + ], + shouldContain: ['InfluxDB v2'], + shouldNotContain: ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'], + }, + { + name: 'Modern Self-hosted SQL User', + answers: ['Self-hosted', 'Paid', 'Less than 6 months', 'SQL'], + shouldContain: ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'], + shouldNotContain: [], + }, + { + name: 'High Volume Enterprise User', + answers: ['Self-hosted', 'Paid', 'Less than 6 months', 'SQL', 'Yes'], + shouldContain: ['InfluxDB 3 Enterprise'], + shouldNotContain: [], + }, + { + name: 'Uncertain User', + answers: ["I'm not sure", "I'm not sure", "I'm not sure", "I'm not sure"], + shouldContain: [], // Should still provide some recommendations + shouldNotContain: [], + }, + ]; + + scenarios.forEach((scenario) => { + cy.visitVersionDetector(); + cy.startQuestionnaire(); + cy.completeQuestionnaire(scenario.answers); + cy.verifyQuestionnaireResults({ + shouldContain: scenario.shouldContain, + shouldNotContain: scenario.shouldNotContain, + }); + }); +}); + +/** + * Test accessibility features + */ +Cypress.Commands.add('testAccessibility', () => { + // Test keyboard navigation + cy.get('body').tab(); + cy.focused().should('have.id', 'influxdb-url'); + + cy.focused().type('https://test.com'); + cy.focused().tab(); + cy.focused().should('have.class', 'submit-button'); + + cy.focused().type('{enter}'); + cy.get('.question.active', { timeout: 3000 }).should('be.visible'); + + // Test that buttons are focusable + cy.get('.option-button') + .first() + .should('be.visible') + .focus() + .should('be.focused'); +}); + +/** + * Test theme integration + */ +Cypress.Commands.add('testThemeIntegration', () => { + // Test light theme (default) + cy.get('[data-component="influxdb-version-detector"]') + .should('have.css', 'background-color') + .and('not.equal', 'transparent'); + + cy.get('.detector-title') + .should('have.css', 'color') + .and('not.equal', 'rgb(0, 0, 0)'); + + // Test dark theme if theme switcher exists + cy.get('body').then(($body) => { + if ($body.find('[data-theme-toggle]').length > 0) { + cy.get('[data-theme-toggle]').click(); + + cy.get('[data-component="influxdb-version-detector"]') + .should('have.css', 'background-color') + .and('not.equal', 'rgb(255, 255, 255)'); + } + }); +}); diff --git a/cypress/support/run-e2e-specs.js b/cypress/support/run-e2e-specs.js index 71f1616fa..1a3d3934e 100644 --- a/cypress/support/run-e2e-specs.js +++ b/cypress/support/run-e2e-specs.js @@ -2,8 +2,7 @@ * InfluxData Documentation E2E Test Runner * * This script automates running Cypress end-to-end tests for the InfluxData documentation site. - * It handles starting a local Hugo server, mapping content files to their URLs, and running Cypress tests, - * and reporting broken links. + * It handles starting a local Hugo server, mapping content files to their URLs, and running Cypress tests. * * Usage: node run-e2e-specs.js [file paths...] [--spec test specs...] */ @@ -303,7 +302,7 @@ async function main() { try { const screenshotsDir = path.resolve('cypress/screenshots'); const videosDir = path.resolve('cypress/videos'); - const specScreenshotDir = path.join(screenshotsDir, 'article-links.cy.js'); + const specScreenshotDir = path.join(screenshotsDir, 'content'); // Ensure base directories exist ensureDirectoryExists(screenshotsDir); @@ -402,7 +401,7 @@ async function main() { if (testFailureCount > 0) { console.warn( - `ℹ️ Note: ${testFailureCount} test(s) failed but no broken links were detected in the report.` + `ℹ️ Note: ${testFailureCount} test(s) failed.` ); // Provide detailed failure analysis diff --git a/data/products.yml b/data/products.yml index 42dfb42d9..2dcf79116 100644 --- a/data/products.yml +++ b/data/products.yml @@ -8,6 +8,20 @@ influxdb3_core: latest: core latest_patch: 3.5.0 placeholder_host: localhost:8181 + detector_config: + query_languages: + SQL: + required_params: ['Host', 'Database'] + optional_params: [] + InfluxQL: + required_params: ['Host', 'Database'] + optional_params: [] + characteristics: ['Free', 'Self-hosted', 'SQL/InfluxQL', 'No auth required', 'Databases'] + detection: + ping_headers: + x-influxdb-version: '^3\.' + x-influxdb-build: 'Core' + url_contains: ['localhost:8181'] ai_sample_questions: - How do I install and run InfluxDB 3 Core? - How do I write a plugin for the Python Processing engine? @@ -23,6 +37,20 @@ influxdb3_enterprise: latest: enterprise latest_patch: 3.5.0 placeholder_host: localhost:8181 + detector_config: + query_languages: + SQL: + required_params: ['Host', 'Database', 'Token'] + optional_params: [] + InfluxQL: + required_params: ['Host', 'Database', 'Token'] + optional_params: [] + characteristics: ['Paid', 'Self-hosted', 'SQL/InfluxQL', 'Token', 'Databases'] + detection: + ping_headers: + x-influxdb-version: '^3\.' + x-influxdb-build: 'Enterprise' + url_contains: ['localhost:8181'] ai_sample_questions: - How do I install and run InfluxDB 3 Enterprise? - Help me write a plugin for the Python Processing engine? @@ -51,6 +79,20 @@ influxdb3_cloud_serverless: list_order: 2 latest: cloud-serverless placeholder_host: cloud2.influxdata.com + detector_config: + query_languages: + SQL: + required_params: ['Host', 'Bucket', 'Token'] + optional_params: [] + InfluxQL: + required_params: ['Host', 'Bucket', 'Token'] + optional_params: [] + Flux: + required_params: ['Host', 'Organization', 'Token', 'Default bucket'] + optional_params: [] + characteristics: ['Paid/Free', 'Cloud', 'All languages', 'Token', 'Buckets'] + detection: + url_contains: ['us-east-1-1.aws.cloud2.influxdata.com', 'eu-central-1-1.aws.cloud2.influxdata.com'] ai_sample_questions: - How do I migrate from InfluxDB Cloud 2 to InfluxDB Cloud Serverless? - What tools can I use to write data to InfluxDB Cloud Serverless? @@ -66,6 +108,17 @@ influxdb3_cloud_dedicated: link: "https://www.influxdata.com/contact-sales-cloud-dedicated/" latest_cli: 2.10.5 placeholder_host: cluster-id.a.influxdb.io + detector_config: + query_languages: + SQL: + required_params: ['Host', 'Database', 'Token'] + optional_params: [] + InfluxQL: + required_params: ['Host', 'Database', 'Token'] + optional_params: [] + characteristics: ['Paid', 'Cloud', 'SQL/InfluxQL', 'Token', 'Databases'] + detection: + url_contains: ['influxdb.io'] ai_sample_questions: - How do I migrate from InfluxDB v1 to InfluxDB Cloud Dedicated? - What tools can I use to write data to Cloud Dedicated? @@ -81,6 +134,18 @@ influxdb3_clustered: latest: clustered link: "https://www.influxdata.com/contact-sales-influxdb-clustered/" placeholder_host: cluster-host.com + detector_config: + query_languages: + SQL: + required_params: ['Host', 'Database', 'Token'] + optional_params: [] + InfluxQL: + required_params: ['URL', 'Database', 'Token'] + optional_params: [] + characteristics: ['Paid', 'Self-hosted', 'SQL/InfluxQL', 'Token', 'Databases'] + detection: + ping_headers: + x-influxdb-version: 'influxqlbridged-development' ai_sample_questions: - How do I use a Helm chart to configure Clustered? - What tools can I use to write data to Clustered? @@ -103,6 +168,20 @@ influxdb: v1: 1.12.2 latest_cli: v2: 2.7.5 + detector_config: + query_languages: + InfluxQL: + required_params: ['URL', 'Database', 'Auth Type (Basic or Token)'] + optional_params: [] + Flux: + required_params: ['URL', 'Token', 'Default bucket'] + optional_params: [] + characteristics: ['Free', 'Self-hosted', 'InfluxQL/Flux', 'Token or Username/Password', 'Buckets'] + detection: + ping_headers: + x-influxdb-build: 'OSS' + x-influxdb-version: '^(1|2)\.' + url_contains: ['localhost:8086'] ai_sample_questions: - How do I write and query data with InfluxDB v2 OSS? - How can I migrate from InfluxDB v2 OSS to InfluxDB 3 Core? @@ -117,6 +196,17 @@ influxdb_cloud: list_order: 1 latest: cloud placeholder_host: cloud2.influxdata.com + detector_config: + query_languages: + InfluxQL: + required_params: ['URL', 'Database', 'Token'] + optional_params: [] + Flux: + required_params: ['URL', 'Organization', 'Token', 'Default bucket'] + optional_params: [] + characteristics: ['Paid/Free', 'Cloud', 'InfluxQL/Flux', 'Token', 'Databases/Buckets'] + detection: + url_contains: ['us-west-2-1.aws.cloud2.influxdata.com', 'us-west-2-2.aws.cloud2.influxdata.com', 'us-east-1-1.aws.cloud2.influxdata.com', 'eu-central-1-1.aws.cloud2.influxdata.com', 'us-central1-1.gcp.cloud2.influxdata.com', 'westeurope-1.azure.cloud2.influxdata.com', 'eastus-1.azure.cloud2.influxdata.com'] ai_sample_questions: - How do I write and query data with InfluxDB Cloud 2? - How is Cloud 2 different from Cloud Serverless? @@ -186,6 +276,18 @@ enterprise_influxdb: latest: v1.12 latest_patches: v1: 1.12.2 + detector_config: + query_languages: + InfluxQL: + required_params: ['URL', 'Database', 'User', 'Password'] + optional_params: [] + Flux: + required_params: ['URL', 'User', 'Password', 'Default database'] + optional_params: [] + characteristics: ['Paid', 'Self-hosted', 'InfluxQL/Flux', 'Username/Password', 'Databases'] + detection: + ping_headers: + x-influxdb-build: 'Enterprise' ai_sample_questions: - How can I configure my InfluxDB v1 Enterprise server? - How do I replicate data between InfluxDB v1 Enterprise and OSS? diff --git a/eslint.config.js b/eslint.config.js index 18764ab0e..bd99c171f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -150,8 +150,19 @@ export default [ }, { files: ['**/*.ts'], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + project: './tsconfig.json', + }, + }, rules: { - // Rules specific to TypeScript files + // TypeScript-specific rules + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'warn', }, }, { @@ -160,6 +171,7 @@ export default [ '**/node_modules/**', '**/public/**', '**/resources/**', + '**/dist/**', '**/.hugo_build.lock', ], }, diff --git a/layouts/partials/footer/modals.html b/layouts/partials/footer/modals.html index 4f6fc5b5b..9830135dd 100644 --- a/layouts/partials/footer/modals.html +++ b/layouts/partials/footer/modals.html @@ -6,6 +6,7 @@ {{ partial "footer/modals/influxdb-url.html" . }} + {{ partial "footer/modals/influxdb-version-detector.html" . }} {{ partial "footer/modals/page-feedback.html" . }} {{ if or (.Page.HasShortcode "influxdb/custom-timestamps") (.Page.HasShortcode "influxdb/custom-timestamps-span") }} {{ partial "footer/modals/influxdb-gs-date-select.html" . }} diff --git a/layouts/partials/footer/modals/influxdb-version-detector.html b/layouts/partials/footer/modals/influxdb-version-detector.html new file mode 100644 index 000000000..29f92e12e --- /dev/null +++ b/layouts/partials/footer/modals/influxdb-version-detector.html @@ -0,0 +1,28 @@ +{{/* + InfluxDB Version Detector Modal Template + + This modal contains the interactive version detector component that helps users + identify which InfluxDB product they're using through a guided questionnaire. +*/}} + +{{/* Process products data into detector format */}} +{{ $detectorProducts := dict }} +{{ range $key, $product := site.Data.products }} + {{ if $product.detector_config }} + {{/* Include detector_config plus name and placeholder_host for configuration guidance */}} + {{ $productData := $product.detector_config }} + {{ if $product.name }} + {{ $productData = merge $productData (dict "name" $product.name) }} + {{ end }} + {{ if $product.placeholder_host }} + {{ $productData = merge $productData (dict "placeholder_host" $product.placeholder_host) }} + {{ end }} + {{ $detectorProducts = merge $detectorProducts (dict $key $productData) }} + {{ end }} +{{ end }} + + \ No newline at end of file diff --git a/layouts/shortcodes/influxdb-version-detector.html b/layouts/shortcodes/influxdb-version-detector.html new file mode 100644 index 000000000..2b9c90321 --- /dev/null +++ b/layouts/shortcodes/influxdb-version-detector.html @@ -0,0 +1,61 @@ +{{/* + InfluxDB Version Detector Modal Trigger Shortcode + Usage: {{< influxdb-version-detector >}} + Usage with Ask AI integration: {{< influxdb-version-detector context="grafana" ai_instruction="Help me use [context] with [influxdb_version]" >}} + + This shortcode creates a button that opens a modal with the InfluxDB version detector component. + The component helps users identify which InfluxDB product they're using through a guided questionnaire. + + Parameters: + - context: Optional context for Ask AI integration (e.g., "grafana") + - ai_instruction: Optional instruction template for Ask AI with placeholders [context] and [influxdb_version] +*/}} + +{{ $context := .Get "context" }} +{{ $aiInstruction := .Get "ai_instruction" }} + +

Identify your InfluxDB version

+ +

+If you are unsure which InfluxDB product you are using, use our interactive version detector to help identify it:

+ +

+ {{ if $context }} + + Detect my InfluxDB version + + {{ else }} + + Detect my InfluxDB version + + {{ end }} +

+ +{{ if and $context $aiInstruction }} + +{{ end }} \ No newline at end of file diff --git a/lefthook.yml b/lefthook.yml index 67db3a771..202152a37 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -82,6 +82,10 @@ pre-commit: run: '.ci/vale/vale.sh --config=content/influxdb/v2/.vale.ini --minAlertLevel=error {staged_files}' + build-typescript: + glob: "assets/js/*.ts" + run: yarn build:ts + stage_fixed: true prettier: tags: [frontend, style] glob: '*.{css,js,ts,jsx,tsx}' @@ -108,7 +112,7 @@ pre-push: - content/example.md run: | echo "Running shortcode examples test due to changes in: {staged_files}" - node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/article-links.cy.js" content/example.md + node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/index.cy.js" content/example.md exit $? # Manage Docker containers diff --git a/package.json b/package.json index 39c97e11e..118893cd3 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "scripts": { "build:pytest:image": "docker build -t influxdata/docs-pytest:latest -f Dockerfile.pytest .", "build:agent:instructions": "node ./helper-scripts/build-agent-instructions.js", + "build:ts": "tsc --project tsconfig.json --outDir dist", + "build:ts:watch": "tsc --project tsconfig.json --outDir dist --watch", "lint": "LEFTHOOK_EXCLUDE=test lefthook run pre-commit && lefthook run pre-push", "pre-commit": "lefthook run pre-commit", "test": "echo \"Run 'yarn test:e2e', 'yarn test:links', 'yarn test:codeblocks:all' or a specific test command. e2e and links test commands can take a glob of file paths to test. Some commands run automatically during the git pre-commit and pre-push hooks.\" && exit 0", @@ -55,7 +57,7 @@ "test:codeblocks:v2": "docker compose run --rm --name v2-pytest v2-pytest", "test:codeblocks:stop-monitors": "./test/scripts/monitor-tests.sh stop cloud-dedicated-pytest && ./test/scripts/monitor-tests.sh stop clustered-pytest", "test:e2e": "node cypress/support/run-e2e-specs.js", - "test:shortcode-examples": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/example.md" + "test:shortcode-examples": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/index.cy.js\" content/example.md" }, "type": "module", "browserslist": [ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..cdc23e314 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ES2020", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./assets/js", + "allowJs": true, + "checkJs": false, + "noEmit": false, + "isolatedModules": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true + }, + "include": [ + "assets/js/**/*.ts" + ], + "exclude": [ + "node_modules", + "public", + "resources", + "dist" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f291dcef6..cb8034ef8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1126,9 +1126,9 @@ callsites@^3.0.0: integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001737: - version "1.0.30001739" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz#b34ce2d56bfc22f4352b2af0144102d623a124f4" - integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA== + version "1.0.30001745" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz" + integrity sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ== careful-downloader@^3.0.0: version "3.0.0"