Merge pull request #6399 from influxdata/feature/influxdb-version-detector

Feature/influxdb version detector
6442-v3-odbc-powerbi
Jason Stirnaman 2025-10-04 16:03:53 -05:00 committed by GitHub
commit 9833b9bf90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 5076 additions and 15 deletions

545
.github/agents/typescript-hugo-agent.md vendored Normal file
View File

@ -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<ExampleComponentConfig>;
private elements: ExampleComponentElements;
private state: Map<string, unknown> = new Map();
constructor(element: HTMLElement, config: ExampleComponentConfig = {}) {
this.elements = { root: element };
this.config = this.mergeConfig(config);
this.init();
}
private mergeConfig(config: ExampleComponentConfig): Required<ExampleComponentConfig> {
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<void> {
// 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" }}
<script src="{{ $ts.RelPermalink }}" defer></script>
{{ else }}
{{ $ts = $ts | fingerprint }}
<script src="{{ $ts.RelPermalink }}" integrity="{{ $ts.Data.Integrity }}" defer></script>
{{ end }}
```
#### Build Performance Optimization
```typescript
// utils/lazy-loader.ts
export class LazyLoader {
private static cache = new Map<string, Promise<any>>();
static async loadComponent(name: string): Promise<any> {
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<void> {
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<string, unknown>;
}
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<string, unknown>) => {
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<string, unknown>): Chainable<void>;
}
}
```
### 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<T extends Record<string, unknown>> {
private state: T;
private listeners: Set<(state: T) => void> = new Set();
constructor(initialState: T) {
this.state = { ...initialState };
}
get current(): Readonly<T> {
return Object.freeze({ ...this.state });
}
update(updates: Partial<T>): 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<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
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)

3
.gitignore vendored
View File

@ -35,6 +35,9 @@ tmp
.idea
**/config.toml
# TypeScript build output
**/dist/
# User context files for AI assistant tools
.context/*
!.context/README.md

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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;
}
}
}
}

View File

@ -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;

View File

@ -34,3 +34,6 @@
"layouts/code-controls",
"layouts/v3-wayfinding";
// Import Components
@import "components/influxdb-version-detector";

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,861 @@
/// <reference types="cypress" />
/**
* 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;
});
});
});
});

View File

@ -22,4 +22,7 @@
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
// Import custom commands for InfluxDB Version Detector
import './influxdb-version-detector-commands.js';

View File

@ -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)');
}
});
});

View File

@ -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

View File

@ -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?

View File

@ -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',
],
},

View File

@ -6,6 +6,7 @@
<a id="modal-close"href="#"><span class="icon-remove"></span></a>
<!-- Modal window content blocks-->
{{ 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" . }}

View File

@ -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 }}
<div class="modal-content" id="influxdb-version-detector">
<div data-component="influxdb-version-detector"
data-products='{{ $detectorProducts | jsonify }}'
data-influxdb-urls='{{ site.Data.influxdb_urls | jsonify }}'></div>
</div>

View File

@ -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" }}
<h4>Identify your InfluxDB version</h4>
<p>
If you are unsure which InfluxDB product you are using, use our interactive version detector to help identify it:</p>
<p>
{{ if $context }}
<a href="#"
class="btn influxdb-detector-trigger"
data-context="{{ $context }}"
{{ if $aiInstruction }}data-ai-instruction="{{ $aiInstruction }}"{{ end }}
onclick="
if (window.gtag) {
window.gtag('event', 'influxdb_version_detector', {
'custom_map.interaction_type': 'modal_trigger',
'custom_map.section': location.pathname,
'custom_map.context': '{{ $context }}'
});
}
window.influxdatadocs.toggleModal('#influxdb-version-detector');
return false;
">
Detect my InfluxDB version
</a>
{{ else }}
<a href="#"
class="btn influxdb-detector-trigger"
onclick="
if (window.gtag) {
window.gtag('event', 'influxdb_version_detector', {
'custom_map.interaction_type': 'modal_trigger',
'custom_map.section': location.pathname
});
}
window.influxdatadocs.toggleModal('#influxdb-version-detector');
return false;
">
Detect my InfluxDB version
</a>
{{ end }}
</p>
{{ if and $context $aiInstruction }}
<!-- Ask AI integration will be implemented in the TypeScript component -->
{{ end }}

View File

@ -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

View File

@ -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": [

34
tsconfig.json Normal file
View File

@ -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"
]
}

View File

@ -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"