2408 lines
84 KiB
TypeScript
2408 lines
84 KiB
TypeScript
/**
|
|
* InfluxDB Version Detector Component
|
|
*
|
|
* Helps users identify which InfluxDB product they're using through a
|
|
* guided questionnaire with URL detection and scoring-based recommendations.
|
|
*
|
|
* DECISION TREE LOGIC (from .context/drafts/influxdb-version-detector/influxdb-decision-tree.md):
|
|
*
|
|
* ## Primary Detection Flow
|
|
*
|
|
* START: User enters URL
|
|
* |
|
|
* ├─→ URL matches known cloud patterns?
|
|
* │ │
|
|
* │ ├─→ YES: Contains "influxdb.io" → **InfluxDB Cloud Dedicated** ✓
|
|
* │ ├─→ YES: Contains "cloud2.influxdata.com" regions → **InfluxDB Cloud Serverless** ✓
|
|
* │ ├─→ YES: Contains "influxcloud.net" → **InfluxDB Cloud 1** ✓
|
|
* │ └─→ YES: Contains other cloud2 regions → **InfluxDB Cloud (TSM)** ✓
|
|
* │
|
|
* └─→ NO: Check port and try /ping endpoint
|
|
* │
|
|
* ├─→ Port 8181 detected? → Strong indicator of v3 (Core/Enterprise)
|
|
* | | Returns 200 (auth successful or disabled)?
|
|
* | │ │--> `x-influxdb-build: Enterprise` -> **InfluxDB 3 Enterprise** ✓ (definitive)
|
|
* | │ │--> `x-influxdb-build: Core` -> **InfluxDB 3 Core** ✓ (definitive)
|
|
* │ │
|
|
* │ ├─→ Returns 401 Unauthorized (default - auth required)?
|
|
* │ │
|
|
* │ └─→ Ask "Paid or Free?"
|
|
* │ ├─→ Paid → **InfluxDB 3 Enterprise** ✓ (definitive)
|
|
* │ └─→ Free → **InfluxDB 3 Core** ✓ (definitive)
|
|
* |
|
|
* ├─→ Port 8086 detected? → Strong indicator of legacy (OSS/Enterprise)
|
|
* │ │ ⚠️ NOTE: v1.x ping auth optional (ping-auth-enabled), v2.x always open
|
|
* │ │
|
|
* │ ├─→ Returns 401 Unauthorized?
|
|
* │ │ │ Could be v1.x with ping-auth-enabled=true OR Enterprise
|
|
* │ │ │
|
|
* │ │ └─→ Ask "Paid or Free?" → Show ranked results
|
|
* │ │
|
|
* │ ├─→ Returns 200/204 (accessible)?
|
|
* │ │ │ Likely v2.x OSS (always open) or v1.x with ping-auth-enabled=false
|
|
* │ │ │
|
|
* │ │ └─→ Continue to questionnaire
|
|
* │
|
|
* └─→ Blocked/Can't detect?
|
|
* │
|
|
* └─→ Start questionnaire
|
|
*
|
|
* ## Questionnaire Flow (No URL or after detection)
|
|
*
|
|
* Q1: Which type of license do you have?
|
|
* ├─→ Paid/Commercial License
|
|
* ├─→ Free/Open Source (including free cloud tiers)
|
|
* └─→ I'm not sure
|
|
*
|
|
* Q2: Is your InfluxDB hosted by InfluxData (cloud) or self-hosted?
|
|
* ├─→ Cloud service (hosted by InfluxData)
|
|
* ├─→ Self-hosted (on your own servers)
|
|
* └─→ I'm not sure
|
|
*
|
|
* Q3: How long has your server been in place?
|
|
* ├─→ Recently installed (less than 1 year)
|
|
* ├─→ 1-5 years
|
|
* ├─→ More than 5 years
|
|
* └─→ I'm not sure
|
|
*
|
|
* Q4: Which query language(s) do you use?
|
|
* ├─→ SQL
|
|
* ├─→ InfluxQL
|
|
* ├─→ Flux
|
|
* ├─→ Multiple languages
|
|
* └─→ I'm not sure
|
|
*
|
|
* ## Definitive Determinations (Stop immediately, no more questions)
|
|
*
|
|
* 1. **401 + Port 8181 + Paid** → InfluxDB 3 Enterprise ✓
|
|
* 2. **401 + Port 8181 + Free** → InfluxDB 3 Core ✓
|
|
* 3. **URL matches cloud pattern** → Specific cloud product ✓
|
|
* 4. **x-influxdb-build header** → Definitive product identification ✓
|
|
*
|
|
* ## Scoring System (When not definitive)
|
|
*
|
|
* ### Elimination Rules
|
|
* - **Free + Self-hosted** → Eliminates all cloud products
|
|
* - **Free** → Eliminates: 3 Enterprise, Enterprise, Clustered, Cloud Dedicated, Cloud 1
|
|
* - **Paid + Self-hosted** → Eliminates all cloud products
|
|
* - **Paid + Cloud** → Eliminates all self-hosted products
|
|
* - **Free + Cloud** → Eliminates all self-hosted products, favors Serverless/TSM
|
|
*
|
|
* ### Strong Signals (High points)
|
|
* - **401 Response**: +50 for v3 products, +30 for Clustered
|
|
* - **Port 8181**: +30 for v3 products
|
|
* - **Port 8086**: +20 for legacy products
|
|
* - **SQL Language**: +40 for v3 products, eliminates v1/v2
|
|
* - **Flux Language**: +30 for v2 era, eliminates v1 and v3
|
|
* - **Server Age 5+ years**: +30 for v1 products, -50 for v3
|
|
*
|
|
* ### Ranking Display Rules
|
|
* - Only show "Most Likely" if:
|
|
* - Top score > 30 (not low confidence)
|
|
* - AND difference between #1 and #2 is ≥ 15 points
|
|
* - Show manual verification commands only if:
|
|
* - Confidence is not high (score < 60)
|
|
* - AND it's a self-hosted product
|
|
* - AND user didn't say it's cloud
|
|
*/
|
|
|
|
import { getInfluxDBUrls } from './services/local-storage.js';
|
|
|
|
interface QueryLanguageConfig {
|
|
required_params: string[];
|
|
optional_params?: string[];
|
|
}
|
|
|
|
interface ProductConfig {
|
|
name?: string;
|
|
query_languages: Record<string, QueryLanguageConfig>;
|
|
characteristics: string[];
|
|
placeholder_host?: string;
|
|
detection?: {
|
|
url_contains?: string[];
|
|
ping_headers?: Record<string, string>;
|
|
};
|
|
}
|
|
|
|
interface Products {
|
|
[key: string]: ProductConfig;
|
|
}
|
|
|
|
interface Answers {
|
|
context?: string | null;
|
|
portClue?: string | null;
|
|
isCloud?: boolean;
|
|
isDocker?: boolean;
|
|
paid?: string;
|
|
hosted?: string;
|
|
age?: string;
|
|
language?: string;
|
|
auth?: string;
|
|
data?: string;
|
|
version?: string;
|
|
[key: string]: string | boolean | null | undefined;
|
|
}
|
|
|
|
interface ComponentOptions {
|
|
component: HTMLElement;
|
|
}
|
|
|
|
interface AnalyticsEventData {
|
|
detected_product?: string;
|
|
detection_method?: string;
|
|
interaction_type: string;
|
|
section?: string;
|
|
completion_status?: string;
|
|
question_id?: string;
|
|
answer_value?: string;
|
|
}
|
|
|
|
// Global gtag function type declaration
|
|
declare global {
|
|
interface Window {
|
|
gtag?: (
|
|
_event: string,
|
|
_action: string,
|
|
_parameters?: Record<string, unknown>
|
|
) => void;
|
|
}
|
|
}
|
|
|
|
class InfluxDBVersionDetector {
|
|
private container: HTMLElement;
|
|
private products: Products;
|
|
private influxdbUrls: Record<string, unknown>;
|
|
private answers: Answers = {};
|
|
private initialized: boolean = false;
|
|
private questionFlow: string[] = [];
|
|
private currentQuestionIndex = 0;
|
|
private questionHistory: string[] = []; // Track question history for back navigation
|
|
private progressBar: HTMLElement | null = null;
|
|
private resultDiv: HTMLElement | null = null;
|
|
private restartBtn: HTMLElement | null = null;
|
|
private currentContext: 'questionnaire' | 'result' = 'questionnaire';
|
|
|
|
constructor(options: ComponentOptions) {
|
|
this.container = options.component;
|
|
|
|
// Parse data attributes from the component element
|
|
const { products, influxdbUrls } = this.parseComponentData();
|
|
|
|
this.products = products;
|
|
this.influxdbUrls = influxdbUrls;
|
|
|
|
// Check if component is in a modal
|
|
const modal = this.container.closest('.modal-content');
|
|
if (modal) {
|
|
// If in modal, wait for modal to be opened before initializing
|
|
this.initializeForModal();
|
|
} else {
|
|
// If not in modal, initialize immediately
|
|
this.init();
|
|
}
|
|
}
|
|
|
|
private parseComponentData(): {
|
|
products: Products;
|
|
influxdbUrls: Record<string, unknown>;
|
|
} {
|
|
let products: Products = {};
|
|
let influxdbUrls: Record<string, unknown> = {};
|
|
|
|
// Parse products data - Hugo always provides this data
|
|
const productsData = this.container.getAttribute('data-products');
|
|
if (productsData) {
|
|
try {
|
|
products = JSON.parse(productsData);
|
|
} catch (error) {
|
|
console.warn('Failed to parse products data:', error);
|
|
}
|
|
}
|
|
|
|
// Parse influxdb URLs data
|
|
const influxdbUrlsData = this.container.getAttribute('data-influxdb-urls');
|
|
if (influxdbUrlsData && influxdbUrlsData !== '#ZgotmplZ') {
|
|
try {
|
|
influxdbUrls = JSON.parse(influxdbUrlsData);
|
|
} catch (error) {
|
|
console.warn('Failed to parse influxdb_urls data:', error);
|
|
influxdbUrls = {}; // Fallback to empty object
|
|
}
|
|
} else {
|
|
console.debug(
|
|
'InfluxDB URLs data not available or blocked by template security. ' +
|
|
'This is expected when Hugo data is unavailable.'
|
|
);
|
|
influxdbUrls = {}; // Fallback to empty object
|
|
}
|
|
|
|
return { products, influxdbUrls };
|
|
}
|
|
|
|
private init(): void {
|
|
this.render();
|
|
this.setupPlaceholders();
|
|
this.attachEventListeners();
|
|
this.showQuestion('q-url-known');
|
|
this.initialized = true;
|
|
|
|
// Track modal opening
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'modal_opened',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
}
|
|
|
|
private setupPlaceholders(): void {
|
|
// This method is called at init but some placeholders need to be set
|
|
// when questions are actually displayed since DOM elements don't exist yet
|
|
}
|
|
|
|
private setupPingHeadersPlaceholder(): void {
|
|
const pingHeaders = this.container.querySelector('#ping-headers');
|
|
if (pingHeaders) {
|
|
const exampleContent = [
|
|
'# Replace this with your actual response headers',
|
|
'# Example formats:',
|
|
'',
|
|
'# InfluxDB 3 Core:',
|
|
'HTTP/1.1 200 OK',
|
|
'x-influxdb-build: core',
|
|
'x-influxdb-version: 3.1.0',
|
|
'',
|
|
'# InfluxDB 3 Enterprise:',
|
|
'HTTP/1.1 200 OK',
|
|
'x-influxdb-build: enterprise',
|
|
'x-influxdb-version: 3.1.0',
|
|
'',
|
|
'# InfluxDB v2 OSS:',
|
|
'HTTP/1.1 204 No Content',
|
|
'X-Influxdb-Build: OSS',
|
|
'X-Influxdb-Version: 2.7.8',
|
|
'',
|
|
'# InfluxDB v1:',
|
|
'HTTP/1.1 204 No Content',
|
|
'X-Influxdb-Version: 1.8.10',
|
|
].join('\n');
|
|
|
|
(pingHeaders as HTMLTextAreaElement).value = exampleContent;
|
|
|
|
// Select all text when user clicks in the textarea so they can easily replace it
|
|
pingHeaders.addEventListener('focus', () => {
|
|
(pingHeaders as HTMLTextAreaElement).select();
|
|
});
|
|
}
|
|
}
|
|
|
|
private setupDockerOutputPlaceholder(): void {
|
|
const dockerOutput = this.container.querySelector('#docker-output');
|
|
if (dockerOutput) {
|
|
const exampleContent = [
|
|
'# Replace this with your actual command output',
|
|
'# Example formats:',
|
|
'',
|
|
'# Version command output:',
|
|
'InfluxDB 3.1.0 (git: abc123def)',
|
|
'or',
|
|
'InfluxDB v2.7.8 (git: 407fa622e)',
|
|
'',
|
|
'# Ping headers from curl -I:',
|
|
'HTTP/1.1 200 OK',
|
|
'x-influxdb-build: core',
|
|
'x-influxdb-version: 3.1.0',
|
|
'',
|
|
'# Startup logs:',
|
|
'2024-01-01T00:00:00.000Z info InfluxDB starting',
|
|
'2024-01-01T00:00:00.000Z info InfluxDB 3.1.0 (git: abc123)',
|
|
].join('\n');
|
|
|
|
(dockerOutput as HTMLTextAreaElement).value = exampleContent;
|
|
|
|
// Select all text when user clicks in the textarea so they can easily replace it
|
|
dockerOutput.addEventListener('focus', () => {
|
|
(dockerOutput as HTMLTextAreaElement).select();
|
|
});
|
|
}
|
|
}
|
|
|
|
private getCurrentPageSection(): string {
|
|
// Extract meaningful section from current page
|
|
const path = window.location.pathname;
|
|
const pathSegments = path.split('/').filter((segment) => segment);
|
|
|
|
// Try to get a meaningful section name
|
|
if (pathSegments.length >= 3) {
|
|
return pathSegments.slice(0, 3).join('/'); // e.g., "influxdb3/core/visualize-data"
|
|
} else if (pathSegments.length >= 2) {
|
|
return pathSegments.slice(0, 2).join('/'); // e.g., "influxdb3/core"
|
|
}
|
|
|
|
return path || 'unknown';
|
|
}
|
|
|
|
private trackAnalyticsEvent(eventData: AnalyticsEventData): void {
|
|
// Track Google Analytics events following the pattern from code-controls.js
|
|
try {
|
|
// Get current page context
|
|
const currentUrl = new URL(window.location.href);
|
|
const path = window.location.pathname;
|
|
|
|
// Determine product context from current page
|
|
let pageContext = 'other';
|
|
if (/\/influxdb\/cloud\//.test(path)) {
|
|
pageContext = 'cloud';
|
|
} else if (/\/influxdb3\/core/.test(path)) {
|
|
pageContext = 'core';
|
|
} else if (/\/influxdb3\/enterprise/.test(path)) {
|
|
pageContext = 'enterprise';
|
|
} else if (/\/influxdb3\/cloud-serverless/.test(path)) {
|
|
pageContext = 'serverless';
|
|
} else if (/\/influxdb3\/cloud-dedicated/.test(path)) {
|
|
pageContext = 'dedicated';
|
|
} else if (/\/influxdb3\/clustered/.test(path)) {
|
|
pageContext = 'clustered';
|
|
} else if (/\/(enterprise_|influxdb).*\/v[1-2]\//.test(path)) {
|
|
pageContext = 'oss/enterprise';
|
|
}
|
|
|
|
// Add tracking parameters to URL (following code-controls.js pattern)
|
|
if (eventData.detected_product) {
|
|
switch (eventData.detected_product) {
|
|
case 'core':
|
|
currentUrl.searchParams.set('dl', 'oss3');
|
|
break;
|
|
case 'enterprise':
|
|
currentUrl.searchParams.set('dl', 'enterprise');
|
|
break;
|
|
case 'cloud':
|
|
case 'cloud-v1':
|
|
case 'cloud-v2-tsm':
|
|
currentUrl.searchParams.set('dl', 'cloud');
|
|
break;
|
|
case 'serverless':
|
|
currentUrl.searchParams.set('dl', 'serverless');
|
|
break;
|
|
case 'dedicated':
|
|
currentUrl.searchParams.set('dl', 'dedicated');
|
|
break;
|
|
case 'clustered':
|
|
currentUrl.searchParams.set('dl', 'clustered');
|
|
break;
|
|
case 'oss':
|
|
case 'oss-v1':
|
|
case 'oss-v2':
|
|
currentUrl.searchParams.set('dl', 'oss');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add additional tracking parameters
|
|
if (eventData.detection_method) {
|
|
currentUrl.searchParams.set(
|
|
'detection_method',
|
|
eventData.detection_method
|
|
);
|
|
}
|
|
if (eventData.completion_status) {
|
|
currentUrl.searchParams.set('completion', eventData.completion_status);
|
|
}
|
|
if (eventData.section) {
|
|
currentUrl.searchParams.set(
|
|
'section',
|
|
encodeURIComponent(eventData.section)
|
|
);
|
|
}
|
|
|
|
// Update browser history without triggering page reload
|
|
if (window.history && window.history.replaceState) {
|
|
window.history.replaceState(null, '', currentUrl.toString());
|
|
}
|
|
|
|
// Send custom Google Analytics event if gtag is available
|
|
if (typeof window.gtag !== 'undefined') {
|
|
window.gtag('event', 'influxdb_version_detector', {
|
|
interaction_type: eventData.interaction_type,
|
|
detected_product: eventData.detected_product,
|
|
detection_method: eventData.detection_method,
|
|
completion_status: eventData.completion_status,
|
|
question_id: eventData.question_id,
|
|
answer_value: eventData.answer_value,
|
|
section: eventData.section,
|
|
page_context: pageContext,
|
|
custom_map: {
|
|
dimension1: eventData.detected_product,
|
|
dimension2: eventData.detection_method,
|
|
dimension3: pageContext,
|
|
},
|
|
});
|
|
}
|
|
} catch (error) {
|
|
// Silently handle analytics errors to avoid breaking functionality
|
|
console.debug('Analytics tracking error:', error);
|
|
}
|
|
}
|
|
|
|
private initializeForModal(): void {
|
|
// Set up event listener to initialize when modal opens
|
|
const modalContent = this.container.closest('.modal-content');
|
|
if (!modalContent) return;
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (
|
|
mutation.type === 'attributes' &&
|
|
mutation.attributeName === 'style'
|
|
) {
|
|
const target = mutation.target as HTMLElement;
|
|
const isVisible =
|
|
target.style.display !== 'none' && target.style.display !== '';
|
|
|
|
if (isVisible && !this.initialized) {
|
|
// Modal just opened and component not yet initialized
|
|
this.init();
|
|
observer.disconnect();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Start observing the modal content for style changes
|
|
observer.observe(modalContent, {
|
|
attributes: true,
|
|
attributeFilter: ['style'],
|
|
});
|
|
|
|
// Also check if modal is already visible
|
|
const computedStyle = window.getComputedStyle(modalContent);
|
|
if (computedStyle.display !== 'none' && !this.initialized) {
|
|
this.init();
|
|
observer.disconnect();
|
|
}
|
|
}
|
|
|
|
private getBasicUrlSuggestion(): string {
|
|
// Provide a basic placeholder URL suggestion based on common patterns
|
|
return 'https://your-influxdb-host.com:8086';
|
|
}
|
|
|
|
private getProductDisplayName(product: string): string {
|
|
const displayNames: Record<string, string> = {
|
|
// Simplified product keys (used in detection results)
|
|
'oss-v1': 'InfluxDB OSS v1.x',
|
|
'oss-v2': 'InfluxDB OSS v2.x',
|
|
oss: 'InfluxDB OSS (version unknown)',
|
|
cloud: 'InfluxDB Cloud',
|
|
'cloud-v1': 'InfluxDB Cloud v1',
|
|
'cloud-v2-tsm': 'InfluxDB Cloud v2 (TSM)',
|
|
serverless: 'InfluxDB Cloud Serverless',
|
|
core: 'InfluxDB 3 Core',
|
|
enterprise: 'InfluxDB 3 Enterprise',
|
|
dedicated: 'InfluxDB Cloud Dedicated',
|
|
clustered: 'InfluxDB Clustered',
|
|
custom: 'Custom URL',
|
|
|
|
// Raw product keys from products.yml (used in scoring)
|
|
influxdb3_core: 'InfluxDB 3 Core',
|
|
influxdb3_enterprise: 'InfluxDB 3 Enterprise',
|
|
influxdb3_cloud_serverless: 'InfluxDB Cloud Serverless',
|
|
influxdb3_cloud_dedicated: 'InfluxDB Cloud Dedicated',
|
|
influxdb3_clustered: 'InfluxDB Clustered',
|
|
influxdb_v1: 'InfluxDB OSS v1.x',
|
|
influxdb_v2: 'InfluxDB OSS v2.x',
|
|
enterprise_influxdb: 'InfluxDB Enterprise v1.x',
|
|
influxdb: 'InfluxDB OSS v2.x',
|
|
};
|
|
displayNames['core or enterprise'] =
|
|
`${displayNames.core} or ${displayNames.enterprise}`;
|
|
return displayNames[product] || product;
|
|
}
|
|
|
|
private generateConfigurationGuidance(productKey: string): string {
|
|
// Map from result product names to products.yml keys
|
|
const productMapping: Record<string, string> = {
|
|
core: 'influxdb3_core',
|
|
enterprise: 'influxdb3_enterprise',
|
|
serverless: 'influxdb3_cloud_serverless',
|
|
dedicated: 'influxdb3_cloud_dedicated',
|
|
clustered: 'influxdb3_clustered',
|
|
'oss-v1': 'influxdb_v1',
|
|
'oss-v2': 'influxdb_v2',
|
|
};
|
|
|
|
const dataKey = productMapping[productKey];
|
|
if (!dataKey || !this.products[dataKey]) {
|
|
return '';
|
|
}
|
|
|
|
const productConfig = this.products[dataKey];
|
|
const productName = this.getProductDisplayName(productKey);
|
|
|
|
if (
|
|
!productConfig.query_languages ||
|
|
Object.keys(productConfig.query_languages).length === 0
|
|
) {
|
|
return '';
|
|
}
|
|
|
|
let html = `
|
|
<div class="configuration-guidance" style="margin-top: 1.5rem; padding: 1rem; background: rgba(var(--article-link-rgb, 59, 130, 246), 0.1); border-left: 4px solid var(--article-link, #3b82f6);">
|
|
<h4 style="margin: 0 0 0.75rem 0; color: var(--article-link, #3b82f6);">Configuration Parameter Meanings for ${productName}</h4>
|
|
<p style="margin: 0 0 1rem 0; font-size: 0.9em;">When configuring Grafana or other tools to connect to your ${productName} instance, these parameters mean:</p>
|
|
`;
|
|
|
|
// Add HOST explanation
|
|
const hostExample = this.getHostExample(dataKey);
|
|
html += `
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<strong>HOST/URL:</strong> The network address where your ${productName} instance is running<br>
|
|
<span style="font-size: 0.85em; color: var(--article-text-secondary, #6b7280);">
|
|
For your setup, this would typically be: <code style="background: rgba(0,0,0,0.1); padding: 0.125rem 0.25rem; border-radius: 3px;">${hostExample}</code>
|
|
</span>
|
|
</div>
|
|
`;
|
|
|
|
// Add database/bucket terminology explanation
|
|
const usesDatabase = this.usesDatabaseTerminology(productConfig);
|
|
if (usesDatabase) {
|
|
html += `
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<strong>DATABASE:</strong> The named collection where your data is stored<br>
|
|
<span style="font-size: 0.85em; color: var(--article-text-secondary, #6b7280);">
|
|
${productName} uses "database" terminology for organizing your time series data
|
|
</span>
|
|
</div>
|
|
`;
|
|
} else {
|
|
html += `
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<strong>BUCKET:</strong> The named collection where your data is stored<br>
|
|
<span style="font-size: 0.85em; color: var(--article-text-secondary, #6b7280);">
|
|
${productName} uses "bucket" terminology for organizing your time series data
|
|
</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Add authentication explanation
|
|
const authInfo = this.getAuthenticationInfo(productConfig);
|
|
html += `
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<strong>AUTHENTICATION:</strong> ${authInfo.description}<br>
|
|
<span style="font-size: 0.85em; color: var(--article-text-secondary, #6b7280);">
|
|
${authInfo.details}
|
|
</span>
|
|
</div>
|
|
`;
|
|
|
|
// Add query language explanation
|
|
const languages = Object.keys(productConfig.query_languages).join(', ');
|
|
html += `
|
|
<div style="margin-bottom: 0;">
|
|
<strong>QUERY LANGUAGE:</strong> The syntax used to retrieve your data<br>
|
|
<span style="font-size: 0.85em; color: var(--article-text-secondary, #6b7280);">
|
|
${productName} supports: ${languages}
|
|
</span>
|
|
</div>
|
|
`;
|
|
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
private getHostExample(productDataKey: string): string {
|
|
// Extract placeholder_host from the products data if available
|
|
const productData = this.products[productDataKey];
|
|
|
|
// Use placeholder_host from the product configuration if available
|
|
if (productData?.placeholder_host) {
|
|
// Add protocol if not present
|
|
const host = productData.placeholder_host;
|
|
if (host.startsWith('http://') || host.startsWith('https://')) {
|
|
return host;
|
|
} else {
|
|
// Default to http for localhost, https for others
|
|
return host.includes('localhost')
|
|
? `http://${host}`
|
|
: `https://${host}`;
|
|
}
|
|
}
|
|
|
|
// Fallback based on product type
|
|
const hostExamples: Record<string, string> = {
|
|
influxdb3_core: 'http://localhost:8181',
|
|
influxdb3_enterprise: 'http://localhost:8181',
|
|
influxdb3_cloud_serverless: 'https://cloud2.influxdata.com',
|
|
influxdb3_cloud_dedicated: 'https://cluster-id.a.influxdb.io',
|
|
influxdb3_clustered: 'https://cluster-host.com',
|
|
influxdb_v1: 'http://localhost:8086',
|
|
influxdb_v2: 'http://localhost:8086',
|
|
};
|
|
|
|
return hostExamples[productDataKey] || 'http://localhost:8086';
|
|
}
|
|
|
|
private usesDatabaseTerminology(productConfig: ProductConfig): boolean {
|
|
// Check if any query language uses 'Database' parameter
|
|
for (const language of Object.values(productConfig.query_languages)) {
|
|
if (language.required_params.includes('Database')) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private getAuthenticationInfo(productConfig: ProductConfig): {
|
|
description: string;
|
|
details: string;
|
|
} {
|
|
// Check if any query language requires Token
|
|
const requiresToken = Object.values(productConfig.query_languages).some(
|
|
(lang) => lang.required_params.includes('Token')
|
|
);
|
|
|
|
// Determine if this product uses "database" or "bucket" terminology
|
|
const usesDatabaseTerm = this.usesDatabaseTerminology(productConfig);
|
|
const resourceName = usesDatabaseTerm ? 'database' : 'bucket';
|
|
|
|
if (requiresToken) {
|
|
return {
|
|
description: 'Token-based authentication required',
|
|
details: `You need a valid API token with appropriate permissions for your ${resourceName}`,
|
|
};
|
|
} else {
|
|
return {
|
|
description: 'No authentication required by default',
|
|
details:
|
|
'This instance typically runs without authentication, though it may be optionally configured',
|
|
};
|
|
}
|
|
}
|
|
|
|
private detectEnterpriseFeatures(): {
|
|
likelyProduct: string;
|
|
confidence: number;
|
|
} | null {
|
|
// According to the decision tree, we cannot reliably distinguish
|
|
// Core vs Enterprise from URL alone. The real differentiator is:
|
|
// - Both Enterprise and Core: /ping requires auth by default (opt-out possible)
|
|
// - Definitive identification requires x-influxdb-build header from 200 response
|
|
//
|
|
// Since this component cannot make HTTP requests to test /ping,
|
|
// we return null to indicate we cannot distinguish them from URL alone.
|
|
|
|
return null;
|
|
}
|
|
|
|
private analyzeUrlPatterns(url: string): {
|
|
likelyProduct: string | null;
|
|
confidence: number;
|
|
suggestion?: string;
|
|
} {
|
|
if (!url || !this.influxdbUrls) {
|
|
return { likelyProduct: null, confidence: 0 };
|
|
}
|
|
|
|
const urlLower = url.toLowerCase();
|
|
|
|
// PRIORITY 1: Check for definitive cloud patterns first (per decision tree)
|
|
// These should be checked before localhost patterns for accuracy
|
|
|
|
// InfluxDB Cloud Dedicated: Contains "influxdb.io"
|
|
if (urlLower.includes('influxdb.io')) {
|
|
return { likelyProduct: 'dedicated', confidence: 1.0 };
|
|
}
|
|
|
|
// InfluxDB Cloud Serverless: Contains "cloud2.influxdata.com" regions
|
|
if (urlLower.includes('cloud2.influxdata.com')) {
|
|
// Check for specific Serverless regions
|
|
const serverlessRegions = [
|
|
'us-east-1-1.aws.cloud2.influxdata.com',
|
|
'eu-central-1-1.aws.cloud2.influxdata.com',
|
|
];
|
|
|
|
for (const region of serverlessRegions) {
|
|
if (urlLower.includes(region.toLowerCase())) {
|
|
return { likelyProduct: 'serverless', confidence: 1.0 };
|
|
}
|
|
}
|
|
|
|
// Other cloud2 regions default to InfluxDB Cloud v2 (TSM)
|
|
return { likelyProduct: 'cloud-v2-tsm', confidence: 0.9 };
|
|
}
|
|
|
|
// InfluxDB Cloud v1 (legacy): Contains "influxcloud.net"
|
|
if (urlLower.includes('influxcloud.net')) {
|
|
return { likelyProduct: 'cloud-v1', confidence: 1.0 };
|
|
}
|
|
|
|
// PRIORITY 2: Check for localhost/port-based patterns (OSS, Core, Enterprise)
|
|
// Note: localhost URLs cannot be cloud versions - they're always self-hosted
|
|
if (urlLower.includes('localhost') || urlLower.includes('127.0.0.1')) {
|
|
// OSS default port
|
|
if (urlLower.includes(':8086')) {
|
|
return {
|
|
likelyProduct: 'oss',
|
|
confidence: 0.8,
|
|
suggestion: 'version-check',
|
|
};
|
|
}
|
|
|
|
// Core/Enterprise default port - both use 8181
|
|
if (urlLower.includes(':8181')) {
|
|
// Try to distinguish between Core and Enterprise
|
|
const enterpriseResult = this.detectEnterpriseFeatures();
|
|
if (enterpriseResult) {
|
|
return enterpriseResult;
|
|
}
|
|
|
|
// Can't distinguish from URL alone - suggest ping test
|
|
return {
|
|
likelyProduct: 'core or enterprise',
|
|
confidence: 0.7,
|
|
suggestion: 'ping-test',
|
|
};
|
|
}
|
|
}
|
|
|
|
// Then check cloud products with provider regions
|
|
// Skip this check if URL is localhost (cannot be cloud)
|
|
const isLocalhost =
|
|
urlLower.includes('localhost') || urlLower.includes('127.0.0.1');
|
|
if (!isLocalhost) {
|
|
for (const [productKey, productData] of Object.entries(
|
|
this.influxdbUrls
|
|
)) {
|
|
if (!productData || typeof productData !== 'object') continue;
|
|
|
|
const providers = (productData as Record<string, unknown>).providers;
|
|
if (!Array.isArray(providers)) continue;
|
|
|
|
for (const provider of providers) {
|
|
if (!provider.regions) continue;
|
|
|
|
for (const region of provider.regions) {
|
|
if (region.url) {
|
|
const patternUrl = region.url.toLowerCase();
|
|
|
|
// Exact match
|
|
if (urlLower === patternUrl) {
|
|
return { likelyProduct: productKey, confidence: 1.0 };
|
|
}
|
|
|
|
// Domain match for cloud URLs
|
|
if (
|
|
productKey === 'cloud' &&
|
|
urlLower.includes('cloud2.influxdata.com')
|
|
) {
|
|
return { likelyProduct: 'cloud', confidence: 0.9 };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Additional heuristics based on common patterns
|
|
// Special handling for user inputs like "cloud 2", "cloud v2", etc.
|
|
// Skip cloud heuristics for localhost URLs
|
|
if (!isLocalhost) {
|
|
if (urlLower.match(/cloud\s*[v]?2/)) {
|
|
return { likelyProduct: 'cloud', confidence: 0.8 };
|
|
}
|
|
|
|
if (
|
|
urlLower.includes('cloud') ||
|
|
urlLower.includes('aws') ||
|
|
urlLower.includes('azure') ||
|
|
urlLower.includes('gcp')
|
|
) {
|
|
return { likelyProduct: 'cloud', confidence: 0.6 };
|
|
}
|
|
}
|
|
|
|
// Port-based suggestions for unknown/invalid URLs
|
|
if (urlLower.includes(':8086')) {
|
|
return {
|
|
likelyProduct: 'oss-port',
|
|
confidence: 0.4,
|
|
suggestion: 'multiple-candidates-8086',
|
|
};
|
|
}
|
|
|
|
if (urlLower.includes(':8181')) {
|
|
return {
|
|
likelyProduct: 'v3-port',
|
|
confidence: 0.4,
|
|
suggestion: 'multiple-candidates-8181',
|
|
};
|
|
}
|
|
|
|
return { likelyProduct: null, confidence: 0 };
|
|
}
|
|
|
|
private render(): void {
|
|
this.container.innerHTML = `
|
|
<div class="influxdb-version-detector">
|
|
<h2 id="detector-title" class="detector-title" tabindex="-1">
|
|
InfluxDB product detector
|
|
</h2>
|
|
<p class="detector-subtitle">
|
|
Answer a few questions to identify which InfluxDB product you're using
|
|
</p>
|
|
|
|
<div class="progress">
|
|
<div class="progress-bar" id="progress-bar" style="width: 0%"></div>
|
|
</div>
|
|
|
|
<div class="question-container">
|
|
<!-- Question: Do you know URL -->
|
|
<div class="question active" id="q-url-known">
|
|
<div class="question-text">
|
|
Do you know the URL of your InfluxDB server?
|
|
</div>
|
|
<button class="option-button"
|
|
data-action="url-known"
|
|
data-value="true">
|
|
Yes, I know the URL
|
|
</button>
|
|
<button class="option-button"
|
|
data-action="url-known"
|
|
data-value="false">
|
|
No, I don't know the URL
|
|
</button>
|
|
<button class="option-button"
|
|
data-action="url-known"
|
|
data-value="airgapped">
|
|
Yes, but it's in an airgapped environment
|
|
</button>
|
|
<button class="option-button"
|
|
data-action="url-known"
|
|
data-value="docker">
|
|
Yes, but it's running in Docker/Kubernetes
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Question: Enter URL -->
|
|
<div class="question" id="q-url-input">
|
|
<div class="question-text">
|
|
Please enter your InfluxDB server URL:
|
|
</div>
|
|
<div class="input-group">
|
|
<input type="url" id="url-input"
|
|
placeholder="for example, https://us-east-1-1.aws.cloud2.influxdata.com or http://localhost:8086">
|
|
</div>
|
|
<button class="back-button" data-action="go-back">Back</button>
|
|
<button class="submit-button"
|
|
data-action="detect-url">Detect Version</button>
|
|
</div>
|
|
|
|
<!-- Question: Manual ping test -->
|
|
<div class="question" id="q-ping-manual">
|
|
<div class="question-text">
|
|
For airgapped environments, run this command from a machine that can
|
|
access your InfluxDB:
|
|
</div>
|
|
<div class="code-block">curl -I http://your-influxdb-url:8086/ping</div>
|
|
<div class="question-text question-text-spaced">
|
|
Then paste the response headers here:
|
|
</div>
|
|
<textarea id="ping-headers">
|
|
</textarea>
|
|
<div class="question-options">
|
|
<button class="back-button" data-action="go-back">Back</button>
|
|
<button class="submit-button"
|
|
data-action="analyze-headers">Analyze Headers</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Question: Docker commands -->
|
|
<div class="question" id="q-docker-manual">
|
|
<div class="question-text">
|
|
For Docker/Kubernetes environments, run these commands to identify your InfluxDB version:
|
|
</div>
|
|
<div class="question-text question-text-spaced">
|
|
First, find your container:
|
|
</div>
|
|
<div class="code-block">docker ps | grep influx</div>
|
|
<div class="question-text question-text-spaced">
|
|
Then run one of these commands (replace <container> with your container name/ID):
|
|
</div>
|
|
<div class="code-block"># Get version info:
|
|
docker exec <container> influxd version
|
|
|
|
# Get ping headers:
|
|
docker exec <container> curl -I localhost:8086/ping
|
|
|
|
# Or check startup logs:
|
|
docker logs <container> 2>&1 | head -20</div>
|
|
<div class="question-text question-text-spaced">
|
|
Paste the output here:
|
|
</div>
|
|
<textarea id="docker-output">
|
|
</textarea>
|
|
<div class="question-options">
|
|
<button class="back-button" data-action="go-back">Back</button>
|
|
<button class="submit-button"
|
|
data-action="analyze-docker">Analyze Output</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Question: Paid vs Free -->
|
|
<div class="question" id="q-paid">
|
|
<div class="question-text">
|
|
Which type of InfluxDB license do you have?
|
|
</div>
|
|
<button class="option-button"
|
|
data-action="answer"
|
|
data-category="paid"
|
|
data-value="paid">
|
|
Paid/Commercial License
|
|
</button>
|
|
<button class="option-button"
|
|
data-action="answer"
|
|
data-category="paid"
|
|
data-value="free">
|
|
Free/Open Source (including free cloud tiers)
|
|
</button>
|
|
<button class="option-button"
|
|
data-action="answer"
|
|
data-category="paid"
|
|
data-value="unknown">
|
|
I'm not sure
|
|
</button>
|
|
<button class="back-button" data-action="go-back">Back</button>
|
|
</div>
|
|
|
|
<!-- Question: Cloud vs Self-hosted -->
|
|
<div class="question" id="q-hosted">
|
|
<div class="question-text">
|
|
Is your InfluxDB instance hosted by InfluxData (cloud) or
|
|
self-hosted?
|
|
</div>
|
|
<button class="option-button"
|
|
data-action="answer"
|
|
data-category="hosted"
|
|
data-value="cloud">
|
|
Cloud service (hosted by InfluxData)
|
|
</button>
|
|
<button class="option-button"
|
|
data-action="answer"
|
|
data-category="hosted"
|
|
data-value="self">
|
|
Self-hosted (on your own servers)
|
|
</button>
|
|
<button class="option-button"
|
|
data-action="answer"
|
|
data-category="hosted"
|
|
data-value="unknown">
|
|
I'm not sure
|
|
</button>
|
|
<button class="back-button" data-action="go-back">Back</button>
|
|
</div>
|
|
|
|
<!-- Question: Server Age -->
|
|
<div class="question" id="q-age">
|
|
<div class="question-text">How long has your InfluxDB server been in place?</div>
|
|
<button class="option-button" data-action="answer" data-category="age" data-value="recent">
|
|
Recently installed (less than 1 year)
|
|
</button>
|
|
<button class="option-button" data-action="answer" data-category="age" data-value="1-5">
|
|
1-5 years
|
|
</button>
|
|
<button class="option-button" data-action="answer" data-category="age" data-value="5+">
|
|
More than 5 years
|
|
</button>
|
|
<button class="option-button" data-action="answer" data-category="age" data-value="unknown">
|
|
I'm not sure
|
|
</button>
|
|
<button class="back-button" data-action="go-back">Back</button>
|
|
</div>
|
|
|
|
<!-- Question: Query Language -->
|
|
<div class="question" id="q-language">
|
|
<div class="question-text">Which query language(s) do you use with InfluxDB?</div>
|
|
<button class="option-button" data-action="answer" data-category="language" data-value="sql">
|
|
SQL
|
|
</button>
|
|
<button class="option-button" data-action="answer" data-category="language" data-value="influxql">
|
|
InfluxQL
|
|
</button>
|
|
<button class="option-button" data-action="answer" data-category="language" data-value="flux">
|
|
Flux
|
|
</button>
|
|
<button class="option-button" data-action="answer" data-category="language" data-value="multiple">
|
|
Multiple languages
|
|
</button>
|
|
<button class="option-button" data-action="answer" data-category="language" data-value="unknown">
|
|
I'm not sure
|
|
</button>
|
|
<button class="back-button" data-action="go-back">Back</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="result" class="result"></div>
|
|
|
|
<button class="submit-button restart-button" data-action="restart" style="display: none;" id="restart-btn">
|
|
Start Over
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// Cache DOM elements
|
|
this.progressBar = this.container.querySelector('#progress-bar');
|
|
this.resultDiv = this.container.querySelector('#result');
|
|
this.restartBtn = this.container.querySelector('#restart-btn');
|
|
}
|
|
|
|
private attachEventListeners(): void {
|
|
this.container.addEventListener('click', (e) => {
|
|
const target = e.target as HTMLElement;
|
|
|
|
if (
|
|
target.classList.contains('option-button') ||
|
|
target.classList.contains('submit-button') ||
|
|
target.classList.contains('back-button')
|
|
) {
|
|
const action = target.dataset.action;
|
|
|
|
switch (action) {
|
|
case 'url-known':
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'question_answered',
|
|
question_id: 'url-known',
|
|
answer_value: target.dataset.value || '',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
this.handleUrlKnown(target.dataset.value);
|
|
break;
|
|
case 'go-back':
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'navigation',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
this.goBack();
|
|
break;
|
|
case 'detect-url':
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'url_detection_attempt',
|
|
detection_method: 'url_analysis',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
this.detectByUrl();
|
|
break;
|
|
case 'analyze-headers':
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'manual_analysis',
|
|
detection_method: 'ping_headers',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
this.analyzePingHeaders();
|
|
break;
|
|
case 'analyze-docker':
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'manual_analysis',
|
|
detection_method: 'docker_output',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
this.analyzeDockerOutput();
|
|
break;
|
|
case 'answer':
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'question_answered',
|
|
question_id: target.dataset.category || '',
|
|
answer_value: target.dataset.value || '',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
this.answerQuestion(
|
|
target.dataset.category!,
|
|
target.dataset.value!
|
|
);
|
|
break;
|
|
case 'auth-help-answer':
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'auth_help_response',
|
|
question_id: target.dataset.category || '',
|
|
answer_value: target.dataset.value || '',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
this.handleAuthorizationHelp(
|
|
target.dataset.category!,
|
|
target.dataset.value!
|
|
);
|
|
break;
|
|
case 'restart':
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'restart',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
this.restart();
|
|
break;
|
|
case 'start-questionnaire': {
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'start_questionnaire',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
// Hide result and restart button first
|
|
if (this.resultDiv) {
|
|
this.resultDiv.classList.remove('show');
|
|
}
|
|
if (this.restartBtn) {
|
|
this.restartBtn.style.display = 'none';
|
|
}
|
|
// Start questionnaire with the detected context
|
|
this.startQuestionnaire(target.dataset.context || null);
|
|
// Focus on the component heading
|
|
const heading = document.getElementById('detector-title');
|
|
if (heading) {
|
|
heading.focus();
|
|
heading.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private updateProgress(): void {
|
|
const totalQuestions = this.questionFlow.length || 5;
|
|
const progress = ((this.currentQuestionIndex + 1) / totalQuestions) * 100;
|
|
if (this.progressBar) {
|
|
this.progressBar.style.width = `${progress}%`;
|
|
}
|
|
}
|
|
|
|
private showQuestion(questionId: string, addToHistory: boolean = true): void {
|
|
const questions = this.container.querySelectorAll('.question');
|
|
questions.forEach((q) => q.classList.remove('active'));
|
|
|
|
const activeQuestion = this.container.querySelector(`#${questionId}`);
|
|
if (activeQuestion) {
|
|
activeQuestion.classList.add('active');
|
|
|
|
// Add smart suggestions for URL input question
|
|
if (questionId === 'q-url-input') {
|
|
this.enhanceUrlInputWithSuggestions();
|
|
}
|
|
}
|
|
|
|
// Track question history for back navigation
|
|
if (addToHistory) {
|
|
this.questionHistory.push(questionId);
|
|
}
|
|
|
|
this.updateProgress();
|
|
}
|
|
|
|
private enhanceUrlInputWithSuggestions(): void {
|
|
const urlInputQuestion = this.container.querySelector('#q-url-input');
|
|
if (!urlInputQuestion) return;
|
|
|
|
const urlInput = urlInputQuestion.querySelector(
|
|
'#url-input'
|
|
) as HTMLInputElement;
|
|
if (!urlInput) return;
|
|
|
|
// Check for existing URL in localStorage
|
|
const storedUrls = getInfluxDBUrls();
|
|
const currentProduct = this.getCurrentProduct();
|
|
const storedUrl = storedUrls[currentProduct] || storedUrls.custom;
|
|
|
|
if (storedUrl && storedUrl !== 'http://localhost:8086') {
|
|
urlInput.value = storedUrl;
|
|
// Add indicator that URL was pre-filled (only if one doesn't already exist)
|
|
const existingIndicator = urlInput.parentElement?.querySelector(
|
|
'.url-prefilled-indicator'
|
|
);
|
|
if (!existingIndicator) {
|
|
const indicator = document.createElement('div');
|
|
indicator.className = 'url-prefilled-indicator';
|
|
indicator.textContent = 'Using previously saved URL';
|
|
urlInput.parentElement?.insertBefore(indicator, urlInput);
|
|
|
|
// Hide indicator when user starts typing
|
|
const originalValue = urlInput.value;
|
|
urlInput.addEventListener('input', () => {
|
|
if (urlInput.value !== originalValue) {
|
|
indicator.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
// Set a basic placeholder suggestion
|
|
const suggestedUrl = this.getBasicUrlSuggestion();
|
|
urlInput.placeholder = `for example, ${suggestedUrl}`;
|
|
}
|
|
}
|
|
|
|
private getCurrentProduct(): string {
|
|
// Try to determine current product context from page or default
|
|
// This could be enhanced to detect from page context
|
|
return 'core'; // Default to core for now
|
|
}
|
|
|
|
private handleUrlKnown(value: string | undefined): void {
|
|
this.currentQuestionIndex++;
|
|
|
|
if (value === 'true') {
|
|
this.showQuestion('q-url-input');
|
|
} else if (value === 'airgapped') {
|
|
this.showQuestion('q-ping-manual');
|
|
// Set up placeholder after question is shown
|
|
setTimeout(() => this.setupPingHeadersPlaceholder(), 0);
|
|
} else if (value === 'docker') {
|
|
this.answers.isDocker = true;
|
|
this.showQuestion('q-docker-manual');
|
|
// Set up placeholder after question is shown
|
|
setTimeout(() => this.setupDockerOutputPlaceholder(), 0);
|
|
} else {
|
|
// Start the questionnaire
|
|
this.answers = {};
|
|
this.questionFlow = ['q-paid', 'q-hosted', 'q-age', 'q-language'];
|
|
this.currentQuestionIndex = 0;
|
|
this.showQuestion('q-paid');
|
|
}
|
|
}
|
|
|
|
private goBack(): void {
|
|
// Remove current question from history
|
|
if (this.questionHistory.length > 0) {
|
|
this.questionHistory.pop();
|
|
}
|
|
|
|
// Go to previous question if available
|
|
if (this.questionHistory.length > 0) {
|
|
const previousQuestion =
|
|
this.questionHistory[this.questionHistory.length - 1];
|
|
// Remove it from history before showing (showQuestion will re-add it)
|
|
this.questionHistory.pop();
|
|
|
|
// Decrement question index
|
|
if (this.currentQuestionIndex > 0) {
|
|
this.currentQuestionIndex--;
|
|
}
|
|
|
|
// Show previous question
|
|
this.showQuestion(previousQuestion);
|
|
} else {
|
|
// No history - go to first question
|
|
this.currentQuestionIndex = 0;
|
|
this.showQuestion('q-url-known');
|
|
}
|
|
}
|
|
|
|
private async detectByUrl(): Promise<void> {
|
|
const urlInput = (
|
|
this.container.querySelector('#url-input') as HTMLInputElement
|
|
)?.value.trim();
|
|
|
|
if (!urlInput) {
|
|
this.showResult('error', 'Please enter a valid URL');
|
|
return;
|
|
}
|
|
|
|
// Use improved URL pattern analysis
|
|
const analysisResult = this.analyzeUrlPatterns(urlInput);
|
|
|
|
// Store URL detection results for scoring system
|
|
if (analysisResult.likelyProduct && analysisResult.likelyProduct !== null) {
|
|
this.answers.detectedProduct = analysisResult.likelyProduct;
|
|
this.answers.detectedConfidence = analysisResult.confidence.toString();
|
|
}
|
|
|
|
if (analysisResult.likelyProduct && analysisResult.likelyProduct !== null) {
|
|
if (analysisResult.suggestion === 'ping-test') {
|
|
// Show ping test suggestion for Core/Enterprise detection
|
|
this.showPingTestSuggestion(urlInput, analysisResult.likelyProduct);
|
|
return;
|
|
} else if (analysisResult.suggestion === 'version-check') {
|
|
// Show OSS version check suggestion
|
|
this.showOSSVersionCheckSuggestion(urlInput);
|
|
return;
|
|
} else if (analysisResult.suggestion === 'multiple-candidates-8086') {
|
|
// Show multiple product suggestions for port 8086
|
|
this.showMultipleCandidatesSuggestion(urlInput, '8086');
|
|
return;
|
|
} else if (analysisResult.suggestion === 'multiple-candidates-8181') {
|
|
// Show multiple product suggestions for port 8181
|
|
this.showMultipleCandidatesSuggestion(urlInput, '8181');
|
|
return;
|
|
} else {
|
|
// Direct detection
|
|
this.showDetectedVersion(analysisResult.likelyProduct);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// URL not recognized - start questionnaire with context
|
|
this.showResult('info', 'Analyzing your InfluxDB server...');
|
|
|
|
// Check if this is a cloud context (like "cloud 2")
|
|
const contextResult = this.detectContext(urlInput);
|
|
if (contextResult.likelyProduct === 'cloud') {
|
|
// Start questionnaire with cloud context
|
|
setTimeout(() => {
|
|
this.startQuestionnaireWithCloudContext();
|
|
}, 2000);
|
|
} else {
|
|
// For other URLs, use the regular questionnaire
|
|
setTimeout(() => {
|
|
this.startQuestionnaire('manual', this.detectPortFromUrl(urlInput));
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
private detectContext(urlInput: string): { likelyProduct?: string } {
|
|
const input = urlInput.toLowerCase();
|
|
|
|
// Check for cloud indicators
|
|
if (input.includes('cloud') || input.includes('influxdata.com')) {
|
|
return { likelyProduct: 'cloud' };
|
|
}
|
|
|
|
// Check for other patterns like "cloud 2"
|
|
if (/cloud\s*[v]?2/.test(input)) {
|
|
return { likelyProduct: 'cloud' };
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
private detectPortFromUrl(urlString: string): string | null {
|
|
try {
|
|
const url = new URL(urlString);
|
|
const port = url.port || (url.protocol === 'https:' ? '443' : '80');
|
|
|
|
if (port === '8181') {
|
|
return 'v3'; // InfluxDB 3 Core/Enterprise typically use 8181
|
|
} else if (port === '8086') {
|
|
return 'legacy'; // OSS v1/v2 or Enterprise v1 typically use 8086
|
|
}
|
|
} catch {
|
|
// Invalid URL
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private startQuestionnaire(
|
|
context: string | null = null,
|
|
portClue: string | null = null
|
|
): void {
|
|
this.answers = {};
|
|
this.answers.context = context;
|
|
this.answers.portClue = portClue;
|
|
this.answers.isCloud = false;
|
|
this.questionFlow = ['q-paid', 'q-age', 'q-language'];
|
|
this.currentQuestionIndex = 0;
|
|
this.showQuestion('q-paid');
|
|
}
|
|
|
|
private startQuestionnaireWithCloudContext(): void {
|
|
this.answers = {};
|
|
this.answers.context = 'cloud';
|
|
this.answers.hosted = 'cloud'; // Pre-set cloud hosting
|
|
this.answers.isCloud = true;
|
|
this.questionFlow = ['q-paid', 'q-age', 'q-language'];
|
|
this.currentQuestionIndex = 0;
|
|
this.showQuestion('q-paid');
|
|
}
|
|
|
|
private answerQuestion(category: string, answer: string): void {
|
|
this.answers[category] = answer;
|
|
|
|
// Determine next question or show results
|
|
if (category === 'paid') {
|
|
if (!this.answers.context) {
|
|
// No URL provided - ask about cloud vs self-hosted
|
|
this.currentQuestionIndex = 1;
|
|
this.showQuestion('q-hosted');
|
|
} else {
|
|
// We have context from URL - go to age
|
|
this.currentQuestionIndex = 1;
|
|
this.showQuestion('q-age');
|
|
}
|
|
} else if (category === 'hosted') {
|
|
this.currentQuestionIndex = 2;
|
|
this.showQuestion('q-age');
|
|
} else if (category === 'age') {
|
|
this.currentQuestionIndex = 3;
|
|
this.showQuestion('q-language');
|
|
} else if (category === 'language') {
|
|
// All questions answered - show ranked results
|
|
this.showRankedResults();
|
|
}
|
|
}
|
|
|
|
private handleAuthorizationHelp(category: string, answer: string): void {
|
|
// Store the answer
|
|
this.answers[category] = answer;
|
|
|
|
// Check if we're in the context of localhost:8181 detection
|
|
// If so, we can provide a high-confidence result
|
|
const currentUrl =
|
|
(
|
|
this.container.querySelector('#url-input') as HTMLInputElement
|
|
)?.value?.toLowerCase() || '';
|
|
const isLocalhost8181 =
|
|
(currentUrl.includes('localhost') || currentUrl.includes('127.0.0.1')) &&
|
|
currentUrl.includes(':8181');
|
|
|
|
if (isLocalhost8181) {
|
|
// For localhost:8181, we can give high-confidence results based on license
|
|
if (answer === 'free') {
|
|
// High confidence it's InfluxDB 3 Core
|
|
const html = `
|
|
<strong>Based on your localhost:8181 server and free license:</strong><br><br>
|
|
${this.generateProductResult('core', true, 'High', false)}
|
|
<div class="action-section" style="margin-top: 1.5rem;">
|
|
<strong>Want to confirm this result?</strong>
|
|
<button class="option-button" data-action="start-questionnaire" data-context="v3-port-detected">
|
|
Use guided questions instead
|
|
</button>
|
|
</div>
|
|
`;
|
|
this.showResult('success', html);
|
|
} else if (answer === 'paid') {
|
|
// High confidence it's InfluxDB 3 Enterprise
|
|
const html = `
|
|
<strong>Based on your localhost:8181 server and paid license:</strong><br><br>
|
|
${this.generateProductResult('enterprise', true, 'High', false)}
|
|
<div class="action-section" style="margin-top: 1.5rem;">
|
|
<strong>Want to confirm this result?</strong>
|
|
<button class="option-button" data-action="start-questionnaire" data-context="v3-port-detected">
|
|
Use guided questions instead
|
|
</button>
|
|
</div>
|
|
`;
|
|
this.showResult('success', html);
|
|
}
|
|
} else {
|
|
// Original behavior for non-localhost:8181 cases
|
|
const resultDiv = this.container.querySelector('#result');
|
|
if (resultDiv) {
|
|
// Add a message about what the license answer means
|
|
const licenseGuidance = document.createElement('div');
|
|
licenseGuidance.className = 'license-guidance';
|
|
licenseGuidance.style.marginTop = '1rem';
|
|
licenseGuidance.style.padding = '0.75rem';
|
|
licenseGuidance.style.backgroundColor =
|
|
'rgba(var(--article-link-rgb, 0, 163, 255), 0.1)';
|
|
licenseGuidance.style.borderLeft =
|
|
'4px solid var(--article-link, #00A3FF)';
|
|
licenseGuidance.style.borderRadius = '4px';
|
|
|
|
if (answer === 'free') {
|
|
licenseGuidance.innerHTML = `
|
|
<strong>Free/Open Source License:</strong>
|
|
<p>This suggests you're using InfluxDB 3 Core or InfluxDB OSS.</p>
|
|
<ul>
|
|
<li><a href="/influxdb3/core/visualize-data/grafana/"
|
|
target="_blank" class="grafana-link">Configure Grafana for InfluxDB 3 Core</a></li>
|
|
<li><a href="/influxdb/v2/visualize-data/grafana/"
|
|
target="_blank" class="grafana-link">Configure Grafana for InfluxDB OSS v2</a></li>
|
|
<li><a href="/influxdb/v1/tools/grafana/"
|
|
target="_blank" class="grafana-link">Configure Grafana for InfluxDB OSS v1</a></li>
|
|
</ul>
|
|
`;
|
|
} else if (answer === 'paid') {
|
|
licenseGuidance.innerHTML = `
|
|
<strong>Paid/Commercial License:</strong>
|
|
<p>This suggests you're using InfluxDB 3 Enterprise or a paid cloud service.</p>
|
|
<ul>
|
|
<li><a href="/influxdb3/enterprise/visualize-data/grafana/"
|
|
target="_blank" class="grafana-link">Configure Grafana for InfluxDB 3 Enterprise</a></li>
|
|
<li><a href="/influxdb3/cloud-dedicated/visualize-data/grafana/"
|
|
target="_blank" class="grafana-link">Configure Grafana for InfluxDB Cloud Dedicated</a></li>
|
|
<li><a href="/influxdb3/cloud-serverless/visualize-data/grafana/"
|
|
target="_blank" class="grafana-link">Configure Grafana for InfluxDB Cloud Serverless</a></li>
|
|
</ul>
|
|
`;
|
|
}
|
|
|
|
// Remove any existing guidance
|
|
const existingGuidance = resultDiv.querySelector('.license-guidance');
|
|
if (existingGuidance) {
|
|
existingGuidance.remove();
|
|
}
|
|
|
|
// Add the new guidance
|
|
resultDiv.appendChild(licenseGuidance);
|
|
|
|
// Focus on the guidance message for accessibility
|
|
licenseGuidance.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
private showRankedResults(): void {
|
|
const scores: Record<string, number> = {};
|
|
|
|
// Initialize all products with base score using their full display names
|
|
// The scoring logic uses full names like 'InfluxDB 3 Core', not keys like 'influxdb3_core'
|
|
Object.entries(this.products).forEach(([key, config]) => {
|
|
const fullName = config.name || key;
|
|
scores[fullName] = 0;
|
|
});
|
|
|
|
// Apply scoring logic based on answers
|
|
this.applyScoring(scores);
|
|
|
|
// Check if user answered "unknown" to all questions
|
|
const allUnknown =
|
|
(!this.answers.paid || this.answers.paid === 'unknown') &&
|
|
(!this.answers.hosted || this.answers.hosted === 'unknown') &&
|
|
(!this.answers.age || this.answers.age === 'unknown') &&
|
|
(!this.answers.language || this.answers.language === 'unknown');
|
|
|
|
// Sort by score and filter out vague products
|
|
const ranked = Object.entries(scores)
|
|
.filter(([product, score]) => {
|
|
// Filter by score threshold
|
|
if (score <= -50) return false;
|
|
// Exclude generic "InfluxDB" product (too vague for results)
|
|
if (product === 'InfluxDB') return false;
|
|
return true;
|
|
})
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 5);
|
|
|
|
// Display results
|
|
this.displayRankedResults(ranked, allUnknown);
|
|
}
|
|
|
|
/**
|
|
* Gets the Grafana documentation link for a given product
|
|
*/
|
|
private getGrafanaLink(productName: string): string | null {
|
|
const GRAFANA_LINKS: Record<string, string> = {
|
|
'InfluxDB 3 Core': '/influxdb3/core/visualize-data/grafana/',
|
|
'InfluxDB 3 Enterprise': '/influxdb3/enterprise/visualize-data/grafana/',
|
|
'InfluxDB Cloud Dedicated':
|
|
'/influxdb3/cloud-dedicated/visualize-data/grafana/',
|
|
'InfluxDB Cloud Serverless':
|
|
'/influxdb3/cloud-serverless/visualize-data/grafana/',
|
|
'InfluxDB OSS 1.x': '/influxdb/v1/tools/grafana/',
|
|
'InfluxDB OSS 2.x': '/influxdb/v2/visualize-data/grafana/',
|
|
'InfluxDB Enterprise': '/influxdb/enterprise/visualize-data/grafana/',
|
|
'InfluxDB Clustered': '/influxdb3/clustered/visualize-data/grafana/',
|
|
'InfluxDB Cloud (TSM)': '/influxdb/cloud/visualize-data/grafana/',
|
|
'InfluxDB Cloud v1': '/influxdb/cloud/visualize-data/grafana/',
|
|
};
|
|
|
|
return GRAFANA_LINKS[productName] || null;
|
|
}
|
|
|
|
/**
|
|
* Generates a unified product result block with characteristics and Grafana link
|
|
*/
|
|
private generateProductResult(
|
|
productName: string,
|
|
isTopResult: boolean = false,
|
|
confidence?: string,
|
|
showRanking?: boolean
|
|
): string {
|
|
const displayName = this.getProductDisplayName(productName) || productName;
|
|
const grafanaLink = this.getGrafanaLink(displayName);
|
|
const resultClass = isTopResult
|
|
? 'product-ranking top-result'
|
|
: 'product-ranking';
|
|
|
|
// Get characteristics from products data
|
|
const characteristics = this.products[productName]?.characteristics;
|
|
|
|
let html = `<div class="${resultClass}">`;
|
|
|
|
if (showRanking) {
|
|
html += `<div class="product-title">${displayName}</div>`;
|
|
if (isTopResult) {
|
|
html += '<span class="most-likely-label">Most Likely</span>';
|
|
}
|
|
} else {
|
|
html += `<div class="product-title">${displayName}</div>`;
|
|
if (isTopResult) {
|
|
html += '<span class="most-likely-label">Detected</span>';
|
|
}
|
|
}
|
|
|
|
// Add characteristics and confidence
|
|
const details = [];
|
|
if (confidence) details.push(`Confidence: ${confidence}`);
|
|
if (characteristics) {
|
|
details.push(characteristics.slice(0, 3).join(', '));
|
|
}
|
|
|
|
if (details.length > 0) {
|
|
html += `<div class="product-details">${details.join(' • ')}</div>`;
|
|
}
|
|
|
|
// Add Grafana link if available
|
|
if (grafanaLink) {
|
|
html += `
|
|
<div class="product-details" style="margin-top: 0.5rem;">
|
|
<a href="${grafanaLink}" target="_blank" class="grafana-link">
|
|
Configure Grafana for ${displayName}
|
|
</a>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += '</div>';
|
|
|
|
// Add configuration guidance for top results
|
|
if (isTopResult) {
|
|
const configGuidance = this.generateConfigurationGuidance(productName);
|
|
if (configGuidance) {
|
|
html += configGuidance;
|
|
}
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
/**
|
|
* Maps simple product keys (used in URL detection) to full product names (used in scoring)
|
|
*/
|
|
private mapProductKeyToFullName(productKey: string): string | null {
|
|
const KEY_TO_FULL_NAME_MAP: Record<string, string> = {
|
|
core: 'InfluxDB 3 Core',
|
|
enterprise: 'InfluxDB 3 Enterprise',
|
|
serverless: 'InfluxDB Cloud Serverless',
|
|
dedicated: 'InfluxDB Cloud Dedicated',
|
|
clustered: 'InfluxDB Clustered',
|
|
'cloud-v2-tsm': 'InfluxDB Cloud (TSM)',
|
|
'cloud-v1': 'InfluxDB Cloud v1',
|
|
oss: 'InfluxDB OSS 2.x',
|
|
'oss-1x': 'InfluxDB OSS 1.x',
|
|
'enterprise-1x': 'InfluxDB Enterprise',
|
|
};
|
|
|
|
return KEY_TO_FULL_NAME_MAP[productKey] || null;
|
|
}
|
|
|
|
private applyScoring(scores: Record<string, number>): void {
|
|
// Product release dates for time-aware scoring
|
|
const PRODUCT_RELEASE_DATES: Record<string, Date> = {
|
|
'InfluxDB 3 Core': new Date('2025-01-01'),
|
|
'InfluxDB 3 Enterprise': new Date('2025-01-01'),
|
|
'InfluxDB Cloud Serverless': new Date('2024-01-01'),
|
|
'InfluxDB Cloud Dedicated': new Date('2024-01-01'),
|
|
'InfluxDB Clustered': new Date('2024-01-01'),
|
|
'InfluxDB OSS 2.x': new Date('2020-11-01'),
|
|
'InfluxDB Cloud (TSM)': new Date('2020-11-01'),
|
|
'InfluxDB OSS 1.x': new Date('2016-09-01'),
|
|
'InfluxDB Enterprise': new Date('2016-09-01'),
|
|
};
|
|
|
|
const currentDate = new Date();
|
|
|
|
// Apply URL detection boost if available
|
|
if (this.answers.detectedProduct && this.answers.detectedConfidence) {
|
|
const detectedProduct = this.answers.detectedProduct as string;
|
|
const confidence =
|
|
typeof this.answers.detectedConfidence === 'number'
|
|
? this.answers.detectedConfidence
|
|
: parseFloat(this.answers.detectedConfidence as string);
|
|
|
|
// Determine confidence boost value
|
|
let boostValue = 0;
|
|
if (confidence >= 1.0) {
|
|
boostValue = 100; // Definitive match
|
|
} else if (confidence >= 0.9) {
|
|
boostValue = 80; // Very high confidence
|
|
} else if (confidence >= 0.7) {
|
|
boostValue = 60; // High confidence
|
|
} else if (confidence >= 0.5) {
|
|
boostValue = 40; // Medium confidence
|
|
}
|
|
|
|
// Handle special case: 'core or enterprise' should boost BOTH products equally
|
|
if (detectedProduct === 'core or enterprise') {
|
|
scores['InfluxDB 3 Core'] += boostValue;
|
|
scores['InfluxDB 3 Enterprise'] += boostValue;
|
|
} else {
|
|
// Normal case: boost single detected product
|
|
const fullProductName = this.mapProductKeyToFullName(detectedProduct);
|
|
if (fullProductName && scores[fullProductName] !== undefined) {
|
|
scores[fullProductName] += boostValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cloud vs self-hosted
|
|
if (this.answers.hosted === 'cloud') {
|
|
scores['InfluxDB 3 Core'] = -1000;
|
|
scores['InfluxDB 3 Enterprise'] = -1000;
|
|
scores['InfluxDB OSS 1.x'] = -1000;
|
|
scores['InfluxDB OSS 2.x'] = -1000;
|
|
scores['InfluxDB Enterprise'] = -1000;
|
|
scores['InfluxDB Clustered'] = -1000;
|
|
} else if (this.answers.hosted === 'self' || !this.answers.isCloud) {
|
|
scores['InfluxDB Cloud Dedicated'] = -1000;
|
|
scores['InfluxDB Cloud Serverless'] = -1000;
|
|
scores['InfluxDB Cloud (TSM)'] = -1000;
|
|
}
|
|
|
|
// Paid vs Free
|
|
if (this.answers.paid === 'free') {
|
|
scores['InfluxDB 3 Core'] += 25;
|
|
scores['InfluxDB OSS 1.x'] += 25;
|
|
scores['InfluxDB OSS 2.x'] += 25;
|
|
scores['InfluxDB'] += 25; // Generic InfluxDB (OSS v2.x)
|
|
scores['InfluxDB Cloud Serverless'] += 10;
|
|
scores['InfluxDB Cloud (TSM)'] += 10;
|
|
|
|
scores['InfluxDB 3 Enterprise'] = -1000;
|
|
scores['InfluxDB Enterprise'] = -1000;
|
|
scores['InfluxDB Clustered'] = -1000;
|
|
scores['InfluxDB Cloud Dedicated'] = -1000;
|
|
} else if (this.answers.paid === 'paid') {
|
|
scores['InfluxDB 3 Enterprise'] += 25;
|
|
scores['InfluxDB Enterprise'] += 20;
|
|
scores['InfluxDB Clustered'] += 15;
|
|
scores['InfluxDB Cloud Dedicated'] += 20;
|
|
scores['InfluxDB Cloud Serverless'] += 15;
|
|
scores['InfluxDB Cloud (TSM)'] += 15;
|
|
|
|
scores['InfluxDB 3 Core'] = -1000;
|
|
scores['InfluxDB OSS 1.x'] = -1000;
|
|
scores['InfluxDB OSS 2.x'] = -1000;
|
|
scores['InfluxDB'] = -1000; // Generic InfluxDB (OSS v2.x)
|
|
}
|
|
|
|
// Time-aware age-based scoring
|
|
Object.entries(scores).forEach(([product]) => {
|
|
const releaseDate = PRODUCT_RELEASE_DATES[product];
|
|
if (!releaseDate) return;
|
|
|
|
const yearsSinceRelease =
|
|
(currentDate.getTime() - releaseDate.getTime()) /
|
|
(365.25 * 24 * 60 * 60 * 1000);
|
|
|
|
if (this.answers.age === 'recent') {
|
|
// Favor products released within last year
|
|
if (yearsSinceRelease < 1) {
|
|
scores[product] += 40; // Very new product
|
|
} else if (yearsSinceRelease < 3) {
|
|
scores[product] += 25; // Relatively new
|
|
}
|
|
} else if (this.answers.age === '1-5') {
|
|
// Check if product existed in this timeframe
|
|
if (yearsSinceRelease >= 1 && yearsSinceRelease <= 5) {
|
|
scores[product] += 25;
|
|
} else if (yearsSinceRelease < 1) {
|
|
scores[product] -= 30; // Too new for this age range
|
|
}
|
|
} else if (this.answers.age === '5+') {
|
|
// Only penalize if product didn't exist 5+ years ago
|
|
if (yearsSinceRelease < 5) {
|
|
scores[product] -= 100; // Product didn't exist 5 years ago
|
|
} else {
|
|
scores[product] += 30; // Product was available 5+ years ago
|
|
}
|
|
}
|
|
});
|
|
|
|
// Query language scoring
|
|
if (this.answers.language === 'sql') {
|
|
scores['InfluxDB 3 Core'] += 40;
|
|
scores['InfluxDB 3 Enterprise'] += 40;
|
|
scores['InfluxDB Cloud Dedicated'] += 30;
|
|
scores['InfluxDB Cloud Serverless'] += 30;
|
|
scores['InfluxDB Clustered'] += 30;
|
|
|
|
scores['InfluxDB OSS 1.x'] = -1000;
|
|
scores['InfluxDB OSS 2.x'] = -1000;
|
|
scores['InfluxDB'] = -1000; // Generic InfluxDB (OSS v2.x)
|
|
scores['InfluxDB Enterprise'] = -1000;
|
|
scores['InfluxDB Cloud (TSM)'] = -1000;
|
|
} else if (this.answers.language === 'flux') {
|
|
scores['InfluxDB OSS 2.x'] += 30;
|
|
scores['InfluxDB'] += 30; // Generic InfluxDB (OSS v2.x)
|
|
scores['InfluxDB Cloud (TSM)'] += 40;
|
|
scores['InfluxDB Cloud Serverless'] += 20;
|
|
scores['InfluxDB Enterprise'] += 20; // v1.x Enterprise supports Flux
|
|
|
|
scores['InfluxDB OSS 1.x'] = -1000;
|
|
scores['InfluxDB 3 Core'] = -1000;
|
|
scores['InfluxDB 3 Enterprise'] = -1000;
|
|
scores['InfluxDB Cloud Dedicated'] = -1000;
|
|
scores['InfluxDB Clustered'] = -1000;
|
|
} else if (this.answers.language === 'influxql') {
|
|
// InfluxQL is supported by all products except pure Flux products
|
|
scores['InfluxDB OSS 1.x'] += 30;
|
|
scores['InfluxDB Enterprise'] += 30;
|
|
scores['InfluxDB OSS 2.x'] += 20;
|
|
scores['InfluxDB'] += 20; // Generic InfluxDB (OSS v2.x)
|
|
scores['InfluxDB Cloud (TSM)'] += 20;
|
|
scores['InfluxDB 3 Core'] += 25;
|
|
scores['InfluxDB 3 Enterprise'] += 25;
|
|
scores['InfluxDB Cloud Dedicated'] += 25;
|
|
scores['InfluxDB Cloud Serverless'] += 25;
|
|
scores['InfluxDB Clustered'] += 25;
|
|
}
|
|
}
|
|
|
|
private displayRankedResults(
|
|
ranked: [string, number][],
|
|
allUnknown: boolean = false
|
|
): void {
|
|
const topScore = ranked[0]?.[1] || 0;
|
|
const secondScore = ranked[1]?.[1] || 0;
|
|
const hasStandout = topScore > 30 && topScore - secondScore >= 15;
|
|
|
|
let html = '';
|
|
|
|
// If all answers were "I'm not sure", show a helpful message
|
|
if (allUnknown) {
|
|
html =
|
|
'<strong>Unable to determine your InfluxDB product</strong><br><br>' +
|
|
'<p>Since you answered "I\'m not sure" to all questions, we don\'t have enough information to identify your InfluxDB product.</p>' +
|
|
'<p>Please check the <strong>InfluxDB version quick reference</strong> table below to identify your product based on its characteristics.</p><br>';
|
|
} else {
|
|
html =
|
|
'<strong>Based on your answers, here are the most likely InfluxDB products:</strong><br><br>';
|
|
}
|
|
|
|
// Only show ranked products if we have meaningful answers
|
|
if (!allUnknown) {
|
|
ranked.forEach(([product, score], index) => {
|
|
const confidence = score > 60 ? 'High' : score > 30 ? 'Medium' : 'Low';
|
|
const isTopResult = index === 0 && hasStandout;
|
|
|
|
// Use unified product result generation with ranking number
|
|
let productHtml = this.generateProductResult(
|
|
product,
|
|
isTopResult,
|
|
confidence,
|
|
true
|
|
);
|
|
|
|
// Add ranking number to the product title
|
|
productHtml = productHtml.replace(
|
|
'<div class="product-title">',
|
|
`<div class="product-title">${index + 1}. `
|
|
);
|
|
|
|
html += productHtml;
|
|
});
|
|
}
|
|
|
|
// Add Quick Reference table (open by default if all answers unknown)
|
|
html += `
|
|
<div class="quick-reference">
|
|
<details${allUnknown ? ' open' : ''}>
|
|
<summary class="reference-summary">
|
|
InfluxDB version quick reference
|
|
</summary>
|
|
<table class="reference-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Product</th>
|
|
<th>License</th>
|
|
<th>Hosting</th>
|
|
<th>Port</th>
|
|
<th>Ping requires auth</th>
|
|
<th>Query languages</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td class="product-name"><a href="/influxdb3/enterprise/">InfluxDB 3 Enterprise</a></td>
|
|
<td>Paid only</td>
|
|
<td>Self-hosted</td>
|
|
<td>8181</td>
|
|
<td>Yes (opt-out)</td>
|
|
<td>SQL, InfluxQL</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="product-name"><a href="/influxdb3/core/">InfluxDB 3 Core</a></td>
|
|
<td>Free only</td>
|
|
<td>Self-hosted</td>
|
|
<td>8181</td>
|
|
<td>Yes (opt-out)</td>
|
|
<td>SQL, InfluxQL</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="product-name"><a href="/enterprise_influxdb/v1/">InfluxDB Enterprise</a></td>
|
|
<td>Paid only</td>
|
|
<td>Self-hosted</td>
|
|
<td>8086</td>
|
|
<td>Yes (required)</td>
|
|
<td>InfluxQL, Flux</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="product-name"><a href="/influxdb3/clustered/">InfluxDB Clustered</a></td>
|
|
<td>Paid only</td>
|
|
<td>Self-hosted</td>
|
|
<td>Varies</td>
|
|
<td>No</td>
|
|
<td>SQL, InfluxQL</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="product-name"><a href="/influxdb/v1/">InfluxDB OSS 1.x</a></td>
|
|
<td>Free only</td>
|
|
<td>Self-hosted</td>
|
|
<td>8086</td>
|
|
<td>No (optional)</td>
|
|
<td>InfluxQL</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="product-name"><a href="/influxdb/v2/">InfluxDB OSS 2.x</a></td>
|
|
<td>Free only</td>
|
|
<td>Self-hosted</td>
|
|
<td>8086</td>
|
|
<td>No</td>
|
|
<td>InfluxQL, Flux</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="product-name"><a href="/influxdb3/cloud-dedicated/">InfluxDB Cloud Dedicated</a></td>
|
|
<td>Paid only</td>
|
|
<td>Cloud</td>
|
|
<td>N/A</td>
|
|
<td>No</td>
|
|
<td>SQL, InfluxQL</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="product-name"><a href="/influxdb3/cloud-serverless/">InfluxDB Cloud Serverless</a></td>
|
|
<td>Free + Paid</td>
|
|
<td>Cloud</td>
|
|
<td>N/A</td>
|
|
<td>N/A</td>
|
|
<td>SQL, InfluxQL, Flux</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="product-name"><a href="/influxdb/cloud/">InfluxDB Cloud (TSM)</a></td>
|
|
<td>Free + Paid</td>
|
|
<td>Cloud</td>
|
|
<td>N/A</td>
|
|
<td>N/A</td>
|
|
<td>InfluxQL, Flux</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</details>
|
|
</div>
|
|
`;
|
|
|
|
this.showResult('success', html);
|
|
}
|
|
|
|
private analyzePingHeaders(): void {
|
|
const headersText = (
|
|
this.container.querySelector('#ping-headers') as HTMLTextAreaElement
|
|
)?.value.trim();
|
|
|
|
if (!headersText) {
|
|
this.showResult('error', 'Please paste the ping response headers');
|
|
return;
|
|
}
|
|
|
|
// Check if user is trying to analyze the example content
|
|
if (
|
|
headersText.includes(
|
|
'# Replace this with your actual response headers'
|
|
) ||
|
|
headersText.includes('# Example formats:')
|
|
) {
|
|
this.showResult(
|
|
'error',
|
|
'Please replace the example content with your actual ping response headers'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check for 401/403 unauthorized responses
|
|
if (headersText.includes('401') || headersText.includes('403')) {
|
|
this.showResult(
|
|
'info',
|
|
`
|
|
<strong>Authentication Required Detected</strong><br><br>
|
|
The ping endpoint requires authentication, which indicates you're likely using one of:<br><br>
|
|
<div class="expected-results">
|
|
<div class="manual-output">
|
|
<strong>InfluxDB 3 Enterprise</strong> - Requires auth by default (opt-out possible)
|
|
</div>
|
|
<div class="manual-output">
|
|
<strong>InfluxDB 3 Core</strong> - Requires auth by default (opt-out possible)
|
|
</div>
|
|
</div>
|
|
Please use the guided questions to narrow down your specific version.
|
|
`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Parse headers and check against patterns
|
|
const headers: Record<string, string> = {};
|
|
headersText.split('\n').forEach((line) => {
|
|
const colonIndex = line.indexOf(':');
|
|
if (colonIndex > -1) {
|
|
const key = line.substring(0, colonIndex).trim().toLowerCase();
|
|
const value = line.substring(colonIndex + 1).trim();
|
|
headers[key] = value;
|
|
}
|
|
});
|
|
|
|
// PRIORITY: Check for definitive x-influxdb-build header (per decision tree)
|
|
const buildHeader = headers['x-influxdb-build'];
|
|
if (buildHeader) {
|
|
if (buildHeader.toLowerCase().includes('enterprise')) {
|
|
this.showDetectedVersion('InfluxDB 3 Enterprise');
|
|
return;
|
|
} else if (buildHeader.toLowerCase().includes('core')) {
|
|
this.showDetectedVersion('InfluxDB 3 Core');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check against product patterns
|
|
let detectedProduct: string | null = null;
|
|
for (const [productName, config] of Object.entries(this.products)) {
|
|
if (config.detection?.ping_headers) {
|
|
let matches = true;
|
|
for (const [header, pattern] of Object.entries(
|
|
config.detection.ping_headers
|
|
)) {
|
|
const regex = new RegExp(pattern);
|
|
if (!headers[header] || !regex.test(headers[header])) {
|
|
matches = false;
|
|
break;
|
|
}
|
|
}
|
|
if (matches) {
|
|
detectedProduct = productName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (detectedProduct) {
|
|
this.showDetectedVersion(detectedProduct);
|
|
} else {
|
|
this.showResult(
|
|
'warning',
|
|
'Unable to determine version from headers. Consider using the guided questions instead.'
|
|
);
|
|
}
|
|
}
|
|
|
|
private showResult(type: string, message: string): void {
|
|
if (this.resultDiv) {
|
|
this.resultDiv.className = `result ${type} show`;
|
|
this.resultDiv.innerHTML = message;
|
|
}
|
|
if (this.restartBtn) {
|
|
this.restartBtn.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
private analyzeDockerOutput(): void {
|
|
const dockerOutput = (
|
|
this.container.querySelector('#docker-output') as HTMLTextAreaElement
|
|
)?.value.trim();
|
|
|
|
if (!dockerOutput) {
|
|
this.showResult('error', 'Please paste the Docker command output');
|
|
return;
|
|
}
|
|
|
|
// Check if user is trying to analyze the example content
|
|
if (
|
|
dockerOutput.includes('# Replace this with your actual command output') ||
|
|
dockerOutput.includes('# Example formats:')
|
|
) {
|
|
this.showResult(
|
|
'error',
|
|
'Please replace the example content with your actual Docker command output'
|
|
);
|
|
return;
|
|
}
|
|
|
|
let detectedProduct: string | null = null;
|
|
|
|
// Check for version patterns in the output
|
|
if (dockerOutput.includes('InfluxDB 3 Core')) {
|
|
detectedProduct = 'InfluxDB 3 Core';
|
|
} else if (dockerOutput.includes('InfluxDB 3 Enterprise')) {
|
|
detectedProduct = 'InfluxDB 3 Enterprise';
|
|
} else if (dockerOutput.includes('InfluxDB v3')) {
|
|
// Generic v3 detection - need more info
|
|
detectedProduct = 'InfluxDB 3 Core or Enterprise';
|
|
} else if (
|
|
dockerOutput.includes('InfluxDB v2') ||
|
|
dockerOutput.includes('InfluxDB 2.')
|
|
) {
|
|
detectedProduct = 'InfluxDB OSS 2.x';
|
|
} else if (
|
|
dockerOutput.includes('InfluxDB v1') ||
|
|
dockerOutput.includes('InfluxDB 1.')
|
|
) {
|
|
if (dockerOutput.includes('Enterprise')) {
|
|
detectedProduct = 'InfluxDB Enterprise';
|
|
} else {
|
|
detectedProduct = 'InfluxDB OSS 1.x';
|
|
}
|
|
}
|
|
|
|
// Also check for ping header patterns (case-insensitive)
|
|
if (!detectedProduct) {
|
|
// First check for x-influxdb-build header (definitive identification)
|
|
const buildMatch = dockerOutput.match(/x-influxdb-build:\s*(\w+)/i);
|
|
if (buildMatch) {
|
|
const build = buildMatch[1].toLowerCase();
|
|
if (build === 'enterprise') {
|
|
detectedProduct = 'InfluxDB 3 Enterprise';
|
|
} else if (build === 'core') {
|
|
detectedProduct = 'InfluxDB 3 Core';
|
|
}
|
|
}
|
|
|
|
// If no build header, check version headers (case-insensitive)
|
|
if (!detectedProduct) {
|
|
const versionMatch = dockerOutput.match(
|
|
/x-influxdb-version:\s*([\d.]+)/i
|
|
);
|
|
if (versionMatch) {
|
|
const version = versionMatch[1];
|
|
if (version.startsWith('3.')) {
|
|
detectedProduct = 'InfluxDB 3 Core or InfluxDB 3Enterprise';
|
|
} else if (version.startsWith('2.')) {
|
|
detectedProduct = 'InfluxDB OSS 2.x';
|
|
} else if (version.startsWith('1.')) {
|
|
detectedProduct = dockerOutput.includes('Enterprise')
|
|
? 'InfluxDB Enterprise'
|
|
: 'InfluxDB OSS 1.x';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (detectedProduct) {
|
|
this.showDetectedVersion(detectedProduct);
|
|
} else {
|
|
this.showResult(
|
|
'warning',
|
|
'Unable to determine version from Docker output. Consider using the guided questions instead.'
|
|
);
|
|
}
|
|
}
|
|
|
|
private showPingTestSuggestion(url: string, productName: string): void {
|
|
// Convert product key to display name
|
|
const displayName = this.getProductDisplayName(productName) || productName;
|
|
const html = `
|
|
<strong>Port 8181 detected - likely ${displayName}</strong><br><br>
|
|
|
|
<p>To distinguish between InfluxDB 3 Core and Enterprise, run one of these commands:</p>
|
|
|
|
<div class="code-block">
|
|
# Direct API call:
|
|
curl -I ${url}/ping
|
|
</div>
|
|
|
|
<details style="margin: 1rem 0;">
|
|
<summary class="expandable-summary">
|
|
View Docker/Container Commands
|
|
</summary>
|
|
<div class="code-block" style="margin-top: 0.5rem;">
|
|
# With Docker Compose:
|
|
docker compose exec influxdb3 curl -I http://localhost:8181/ping
|
|
|
|
# With Docker (replace <container> with your container name):
|
|
docker exec <container> curl -I localhost:8181/ping
|
|
</div>
|
|
</details>
|
|
|
|
<div class="expected-results">
|
|
<div class="results-title">Expected results:</div>
|
|
• <strong>X-Influxdb-Build: Enterprise</strong> → InfluxDB 3 Enterprise (definitive)<br>
|
|
• <strong>X-Influxdb-Build: Core</strong> → InfluxDB 3 Core (definitive)<br>
|
|
• <strong>401 Unauthorized</strong> → Use the license information below
|
|
</div>
|
|
|
|
<div class="authorization-help">
|
|
<div class="results-title">If you get 401 Unauthorized:</div>
|
|
<p><strong>What type of license do you have?</strong></p>
|
|
<button class="option-button compact"
|
|
data-action="auth-help-answer"
|
|
data-category="paid"
|
|
data-value="free">
|
|
Free / Open Source
|
|
</button>
|
|
<button class="option-button compact"
|
|
data-action="auth-help-answer"
|
|
data-category="paid"
|
|
data-value="paid">
|
|
Paid / Commercial
|
|
</button>
|
|
</div>
|
|
|
|
<div class="action-section">
|
|
<strong>Can't run the command?</strong>
|
|
<button class="option-button" data-action="start-questionnaire" data-context="v3-port-detected">
|
|
Use guided questions instead
|
|
</button>
|
|
</div>
|
|
`;
|
|
this.showResult('success', html);
|
|
}
|
|
|
|
private showOSSVersionCheckSuggestion(url: string): void {
|
|
const html = `
|
|
<strong>Port 8086 detected - likely InfluxDB OSS</strong><br><br>
|
|
|
|
<p>To determine if this is InfluxDB OSS v1.x or v2.x, run one of these commands:</p>
|
|
|
|
<div class="code-block">
|
|
# Check version directly:
|
|
influxd version
|
|
|
|
# Or check via API:
|
|
curl -I ${url}/ping
|
|
</div>
|
|
|
|
<div class="expected-results">
|
|
<div class="results-title">Expected version patterns:</div>
|
|
• <strong>v1.x.x</strong> → ${this.getProductDisplayName('oss-v1')}<br>
|
|
• <strong>v2.x.x</strong> → ${this.getProductDisplayName('oss-v2')}<br>
|
|
</div>
|
|
|
|
<details style="margin: 1rem 0;">
|
|
<summary class="expandable-summary">
|
|
Docker/Container Commands
|
|
</summary>
|
|
<div class="code-block" style="margin-top: 0.5rem;">
|
|
# Get version info:
|
|
docker exec <container> influxd version
|
|
|
|
# Get ping headers:
|
|
docker exec <container> curl -I localhost:8086/ping
|
|
|
|
# Or check startup logs:
|
|
docker logs <container> 2>&1 | head -20
|
|
</div>
|
|
<p style="margin-top: 0.5rem; font-size: 0.9em; opacity: 0.8;">
|
|
Replace <container> with your actual container name or ID.
|
|
</p>
|
|
</details>
|
|
|
|
<div class="action-section">
|
|
<strong>Can't run these commands?</strong>
|
|
<button class="option-button" data-action="start-questionnaire" data-context="oss-port-detected">
|
|
Use guided questions instead
|
|
</button>
|
|
</div>
|
|
`;
|
|
this.showResult('success', html);
|
|
}
|
|
|
|
private showMultipleCandidatesSuggestion(url: string, port: string): void {
|
|
let candidates: string[] = [];
|
|
let portDescription = '';
|
|
|
|
if (port === '8086') {
|
|
candidates = [
|
|
'InfluxDB OSS 1.x',
|
|
'InfluxDB OSS 2.x',
|
|
'InfluxDB Enterprise',
|
|
];
|
|
portDescription =
|
|
'Port 8086 is used by InfluxDB OSS v1.x, OSS v2.x, and Enterprise v1.x';
|
|
} else if (port === '8181') {
|
|
candidates = ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'];
|
|
portDescription = 'Port 8181 is used by InfluxDB 3 Core and Enterprise';
|
|
}
|
|
|
|
const candidatesList = candidates
|
|
.map((product) =>
|
|
this.generateProductResult(product, false, 'Medium', false)
|
|
)
|
|
.join('');
|
|
|
|
const html = `
|
|
<strong>Based on the port pattern in your URL, here are the possible products:</strong><br><br>
|
|
|
|
<p style="margin: 1rem 0;">${portDescription}. Without additional information, we cannot determine which specific version you're using.</p>
|
|
|
|
<div class="product-candidates" style="margin: 1rem 0;">
|
|
<strong>Possible products:</strong><br>
|
|
${candidatesList}
|
|
</div>
|
|
|
|
<div class="action-section">
|
|
<strong>To narrow this down:</strong>
|
|
<button class="option-button" data-action="start-questionnaire" data-context="port-detected">
|
|
Answer a few questions
|
|
</button>
|
|
</div>
|
|
`;
|
|
this.showResult('info', html);
|
|
}
|
|
|
|
private showDetectedVersion(productName: string): void {
|
|
// Track successful detection
|
|
this.trackAnalyticsEvent({
|
|
interaction_type: 'product_detected',
|
|
detected_product: productName.toLowerCase().replace(/\s+/g, '_'),
|
|
completion_status: 'success',
|
|
section: this.getCurrentPageSection(),
|
|
});
|
|
|
|
const html = `
|
|
<strong>Based on your input, we believe the InfluxDB product you are using is most likely:</strong><br><br>
|
|
${this.generateProductResult(productName, true, 'High', false)}
|
|
`;
|
|
this.showResult('success', html);
|
|
}
|
|
|
|
private restart(): void {
|
|
this.answers = {};
|
|
this.questionFlow = [];
|
|
this.currentQuestionIndex = 0;
|
|
this.questionHistory = [];
|
|
|
|
// Clear inputs
|
|
const urlInput = this.container.querySelector(
|
|
'#url-input'
|
|
) as HTMLInputElement;
|
|
const pingHeaders = this.container.querySelector(
|
|
'#ping-headers'
|
|
) as HTMLTextAreaElement;
|
|
const dockerOutput = this.container.querySelector(
|
|
'#docker-output'
|
|
) as HTMLTextAreaElement;
|
|
|
|
if (urlInput) urlInput.value = '';
|
|
if (pingHeaders) pingHeaders.value = '';
|
|
if (dockerOutput) dockerOutput.value = '';
|
|
|
|
// Remove URL prefilled indicator if present
|
|
const indicator = this.container.querySelector('.url-prefilled-indicator');
|
|
if (indicator) {
|
|
indicator.remove();
|
|
}
|
|
|
|
// Hide result
|
|
if (this.resultDiv) {
|
|
this.resultDiv.classList.remove('show');
|
|
}
|
|
if (this.restartBtn) {
|
|
this.restartBtn.style.display = 'none';
|
|
}
|
|
|
|
// Show first question
|
|
this.showQuestion('q-url-known');
|
|
|
|
// Reset progress
|
|
if (this.progressBar) {
|
|
this.progressBar.style.width = '0%';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export as component initializer
|
|
export default function initInfluxDBVersionDetector(
|
|
options: ComponentOptions
|
|
): InfluxDBVersionDetector {
|
|
return new InfluxDBVersionDetector(options);
|
|
}
|