Merge pull request #6399 from influxdata/feature/influxdb-version-detector
Feature/influxdb version detector6442-v3-odbc-powerbi
commit
9833b9bf90
|
|
@ -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)
|
||||
|
|
@ -35,6 +35,9 @@ tmp
|
|||
.idea
|
||||
**/config.toml
|
||||
|
||||
# TypeScript build output
|
||||
**/dist/
|
||||
|
||||
# User context files for AI assistant tools
|
||||
.context/*
|
||||
!.context/README.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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -34,3 +34,6 @@
|
|||
"layouts/code-controls",
|
||||
"layouts/v3-wayfinding";
|
||||
|
||||
// Import Components
|
||||
@import "components/influxdb-version-detector";
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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)');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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" . }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 }}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue