fix(version-detector): centralize Grafana links and DRY up host examples (#6693)

* fix(version-detector): use centralized getGrafanaLink for all Grafana URLs

Refactor handleAuthorizationHelp to use getGrafanaLink() instead of
hardcoded URLs, ensuring all Grafana links come from a single source.

Also fix incorrect URLs in getGrafanaLink mapping:
- InfluxDB OSS 2.x: /visualize-data/ → /tools/
- InfluxDB Enterprise: /influxdb/enterprise/ → /enterprise_influxdb/v1/
- InfluxDB Cloud (TSM): /visualize-data/ → /tools/
- InfluxDB Cloud v1: now links to Enterprise v1 docs (Cloud v1 is
  Enterprise under the hood)

* refactor(version-detector): DRY up localhost:8086 references

Extract HOST_EXAMPLES to a class-level constant and add DEFAULT_HOST
and DEFAULT_HOST_PORT constants to eliminate duplicate localhost:8086
strings throughout the code.

- Move hostExamples from local variable to class constant
- Use DEFAULT_HOST for URL placeholder and comparison checks
- Use DEFAULT_HOST_PORT for docker curl command examples

* feat(ask-ai): Support source group IDs in Ask AI trigger links

* feat(version-detector): Present context-aware links

- Add ai_source_group_ids fields to ProductConfig interface
- Improve SCSS for doc and Ask AI links
- Update Grafana docs to add aliases and context param for detector
- Update modal partial to include AI source group IDs in config
- Remove custom Cypress commands for version detector
- Update E2E tests to use direct Cypress commands
pull/6758/head^2 link-checker-v1.3.1
Jason Stirnaman 2026-01-26 18:42:09 -06:00 committed by GitHub
parent fc8c9bbe29
commit 68f00e6805
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 634 additions and 1211 deletions

View File

@ -1,10 +1,10 @@
extends: substitution
message: Use '%s' instead of '%s'
level: warning
ignorecase: false
ignorecase: false
# swap maps tokens in form of bad: good
# NOTE: The left-hand (bad) side can match the right-hand (good) side;
# Vale ignores alerts that match the intended form.
# NOTE: The left-hand (bad) side can match the right-hand (good) side;
# Vale ignores alerts that match the intended form.
swap:
'the compactor': the Compactor
'dedupe': deduplicate

View File

@ -17,7 +17,7 @@ ExecutionPlan
Flight SQL
FlightQuery
GBs?
Grafana|\{\{.*grafana.*\}\}
(?i)Grafana|\{\{.*grafana.*\}\}
HostURL
[Hh]ardcod(e|ed|es|ing)
InfluxDB Cloud

View File

@ -78,6 +78,7 @@ function handleAskAILinks() {
if (!link) return;
const query = link.getAttribute('data-query');
const sourceGroupIds = link.getAttribute('data-source-group-ids');
// Initialize Kapa if not already done
if (!state.kapaInitialized) {
@ -88,20 +89,30 @@ function handleAskAILinks() {
// Give Kapa a moment to initialize
setTimeout(() => {
if (window.Kapa?.open) {
window.Kapa.open({
const openOptions = {
mode: 'ai',
query: query,
});
};
// Add source group IDs if provided
if (sourceGroupIds) {
openOptions.sourceGroupIdsInclude = sourceGroupIds;
}
window.Kapa.open(openOptions);
}
}, 100);
}
} else {
// Kapa is already initialized - open with query if provided
if (query && window.Kapa?.open) {
window.Kapa.open({
const openOptions = {
mode: 'ai',
query: query,
});
};
// Add source group IDs if provided
if (sourceGroupIds) {
openOptions.sourceGroupIdsInclude = sourceGroupIds;
}
window.Kapa.open(openOptions);
}
}
},

View File

@ -122,6 +122,9 @@ interface ProductConfig {
url_contains?: string[];
ping_headers?: Record<string, string>;
};
ai_source_group_ids?: string;
ai_source_group_ids__v1?: string;
[key: string]: unknown; // Allow additional properties
}
interface Products {
@ -161,9 +164,9 @@ interface AnalyticsEventData {
declare global {
interface Window {
gtag?: (
_event: string,
_action: string,
_parameters?: Record<string, unknown>
_command: 'event' | 'config' | 'set',
_targetId: string,
_config?: Record<string, unknown>
) => void;
}
}
@ -181,6 +184,25 @@ class InfluxDBVersionDetector {
private resultDiv: HTMLElement | null = null;
private restartBtn: HTMLElement | null = null;
private currentContext: 'questionnaire' | 'result' = 'questionnaire';
private pageContext: string | null = null; // Context from page (e.g., "grafana")
/** Example host URLs for each product type */
private static readonly HOST_EXAMPLES: 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',
};
/** Default host URL (InfluxDB v2 localhost) */
private static readonly DEFAULT_HOST =
InfluxDBVersionDetector.HOST_EXAMPLES.influxdb_v2;
/** Default host:port without protocol (for curl examples) */
private static readonly DEFAULT_HOST_PORT = 'localhost:8086';
constructor(options: ComponentOptions) {
this.container = options.component;
@ -191,6 +213,9 @@ class InfluxDBVersionDetector {
this.products = products;
this.influxdbUrls = influxdbUrls;
// Check for context from modal trigger button
this.parsePageContext();
// Check if component is in a modal
const modal = this.container.closest('.modal-content');
if (modal) {
@ -202,6 +227,19 @@ class InfluxDBVersionDetector {
}
}
/**
* Parse page context from modal trigger button
*/
private parsePageContext(): void {
// Look for the modal trigger button with data-context attribute
const trigger = document.querySelector(
'.influxdb-detector-trigger[data-context]'
);
if (trigger) {
this.pageContext = trigger.getAttribute('data-context');
}
}
private parseComponentData(): {
products: Products;
influxdbUrls: Record<string, unknown>;
@ -627,17 +665,10 @@ class InfluxDBVersionDetector {
}
// 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';
return (
InfluxDBVersionDetector.HOST_EXAMPLES[productDataKey] ||
InfluxDBVersionDetector.DEFAULT_HOST
);
}
private usesDatabaseTerminology(productConfig: ProductConfig): boolean {
@ -888,7 +919,7 @@ class InfluxDBVersionDetector {
</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">
placeholder="for example, https://us-east-1-1.aws.cloud2.influxdata.com or ${InfluxDBVersionDetector.DEFAULT_HOST}">
</div>
<button class="back-button" data-action="go-back">Back</button>
<button class="submit-button"
@ -930,7 +961,7 @@ class InfluxDBVersionDetector {
docker exec &lt;container&gt; influxd version
# Get ping headers:
docker exec &lt;container&gt; curl -I localhost:8086/ping
docker exec &lt;container&gt; curl -I ${InfluxDBVersionDetector.DEFAULT_HOST_PORT}/ping
# Or check startup logs:
docker logs &lt;container&gt; 2>&amp;1 | head -20</div>
@ -1207,7 +1238,7 @@ docker logs &lt;container&gt; 2>&amp;1 | head -20</div>
const currentProduct = this.getCurrentProduct();
const storedUrl = storedUrls[currentProduct] || storedUrls.custom;
if (storedUrl && storedUrl !== 'http://localhost:8086') {
if (storedUrl && storedUrl !== InfluxDBVersionDetector.DEFAULT_HOST) {
urlInput.value = storedUrl;
// Add indicator that URL was pre-filled (only if one doesn't already exist)
const existingIndicator = urlInput.parentElement?.querySelector(
@ -1491,29 +1522,75 @@ docker logs &lt;container&gt; 2>&amp;1 | head -20</div>
licenseGuidance.style.borderRadius = '4px';
if (answer === 'free') {
const freeProducts = [
'InfluxDB 3 Core',
'InfluxDB OSS 2.x',
'InfluxDB OSS 1.x',
];
let freeLinks = '';
if (this.pageContext === 'grafana') {
freeLinks = freeProducts
.map((product) => {
const link = this.getGrafanaLink(product);
return link
? `<li><a href="${link}" target="_blank" class="doc-link">Configure Grafana for ${product}</a></li>`
: '';
})
.filter(Boolean)
.join('\n ');
} else {
freeLinks = freeProducts
.map((product) => {
const link = this.getDocumentationUrl(product);
return link
? `<li><a href="${link}" target="_blank" class="doc-link">View ${product} documentation</a></li>`
: '';
})
.filter(Boolean)
.join('\n ');
}
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>
${freeLinks}
</ul>
`;
} else if (answer === 'paid') {
const paidProducts = [
'InfluxDB 3 Enterprise',
'InfluxDB Cloud Dedicated',
'InfluxDB Cloud Serverless',
];
let paidLinks = '';
if (this.pageContext === 'grafana') {
paidLinks = paidProducts
.map((product) => {
const link = this.getGrafanaLink(product);
return link
? `<li><a href="${link}" target="_blank" class="doc-link">Configure Grafana for ${product}</a></li>`
: '';
})
.filter(Boolean)
.join('\n ');
} else {
paidLinks = paidProducts
.map((product) => {
const link = this.getDocumentationUrl(product);
return link
? `<li><a href="${link}" target="_blank" class="doc-link">View ${product} documentation</a></li>`
: '';
})
.filter(Boolean)
.join('\n ');
}
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>
${paidLinks}
</ul>
`;
}
@ -1571,24 +1648,94 @@ docker logs &lt;container&gt; 2>&amp;1 | head -20</div>
/**
* Gets the Grafana documentation link for a given product
* Builds on the documentation URL by appending the visualize-data/grafana path
*/
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/',
const docUrl = this.getDocumentationUrl(productName);
if (!docUrl) return null;
return `${docUrl}visualize-data/grafana/`;
}
/**
* Gets the documentation URL for a given product
*/
private getDocumentationUrl(productName: string): string | null {
const DOC_LINKS: Record<string, string> = {
'InfluxDB 3 Core': '/influxdb3/core/',
'InfluxDB 3 Enterprise': '/influxdb3/enterprise/',
'InfluxDB Cloud Dedicated': '/influxdb3/cloud-dedicated/',
'InfluxDB Cloud Serverless': '/influxdb3/cloud-serverless/',
'InfluxDB OSS 1.x': '/influxdb/v1/',
'InfluxDB OSS 2.x': '/influxdb/v2/',
'InfluxDB Enterprise': '/enterprise_influxdb/v1/',
'InfluxDB Clustered': '/influxdb3/clustered/',
'InfluxDB Cloud (TSM)': '/influxdb/cloud/',
'InfluxDB Cloud v1': '/enterprise_influxdb/v1/',
};
return GRAFANA_LINKS[productName] || null;
return DOC_LINKS[productName] || null;
}
/**
* Gets the Ask AI context/product identifier for a given product
*/
private getAskAIContext(productName: string): string | null {
const AI_CONTEXTS: Record<string, string> = {
'InfluxDB 3 Core': 'InfluxDB 3 Core',
'InfluxDB 3 Enterprise': 'InfluxDB 3 Enterprise',
'InfluxDB Cloud Dedicated': 'InfluxDB Cloud Dedicated',
'InfluxDB Cloud Serverless': 'InfluxDB Cloud Serverless',
'InfluxDB OSS 1.x': 'InfluxDB OSS v1',
'InfluxDB OSS 2.x': 'InfluxDB OSS v2',
'InfluxDB Enterprise': 'InfluxDB Enterprise v1',
'InfluxDB Clustered': 'InfluxDB Clustered',
'InfluxDB Cloud (TSM)': 'InfluxDB Cloud (TSM)',
'InfluxDB Cloud v1': 'InfluxDB Cloud v1',
};
return AI_CONTEXTS[productName] || null;
}
/**
* Gets the AI source group IDs for a given product
* Maps display names to product keys to look up source group IDs
*/
private getAISourceGroupIds(productName: string): string | null {
// Map display names to product keys in products.yml
const PRODUCT_KEY_MAP: Record<string, string> = {
'InfluxDB 3 Core': 'influxdb3_core',
'InfluxDB 3 Enterprise': 'influxdb3_enterprise',
'InfluxDB Cloud Dedicated': 'influxdb3_cloud_dedicated',
'InfluxDB Cloud Serverless': 'influxdb3_cloud_serverless',
'InfluxDB OSS 1.x': 'influxdb',
'InfluxDB OSS 2.x': 'influxdb',
'InfluxDB Enterprise': 'enterprise_influxdb',
'InfluxDB Clustered': 'influxdb3_clustered',
'InfluxDB Cloud (TSM)': 'influxdb_cloud',
'InfluxDB Cloud v1': 'enterprise_influxdb',
};
const productKey = PRODUCT_KEY_MAP[productName];
if (!productKey || !this.products[productKey]) return null;
// Handle version-specific source group IDs first
// InfluxDB OSS has different source groups for v1 and v2
if (productName === 'InfluxDB OSS 1.x') {
const v1SourceGroupIds =
this.products[productKey].ai_source_group_ids__v1;
if (typeof v1SourceGroupIds === 'string') {
return v1SourceGroupIds;
}
}
// Get general source group IDs from products data
const sourceGroupIds = this.products[productKey].ai_source_group_ids;
if (typeof sourceGroupIds === 'string') {
return sourceGroupIds;
}
return null;
}
/**
@ -1601,7 +1748,6 @@ docker logs &lt;container&gt; 2>&amp;1 | head -20</div>
showRanking?: boolean
): string {
const displayName = this.getProductDisplayName(productName) || productName;
const grafanaLink = this.getGrafanaLink(displayName);
const resultClass = isTopResult
? 'product-ranking top-result'
: 'product-ranking';
@ -1634,15 +1780,46 @@ docker logs &lt;container&gt; 2>&amp;1 | head -20</div>
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>
`;
// Add context-aware links
if (this.pageContext === 'grafana') {
// Show Grafana-specific link
const grafanaLink = this.getGrafanaLink(displayName);
if (grafanaLink) {
html += `
<div class="product-details" style="margin-top: 0.5rem;">
<a href="${grafanaLink}" target="_blank" class="doc-link">
Configure Grafana for ${displayName}
</a>
</div>
`;
}
} else {
// Show general documentation and Ask AI links
const docLink = this.getDocumentationUrl(displayName);
const aiContext = this.getAskAIContext(displayName);
if (docLink || aiContext) {
html += '<div class="product-details" style="margin-top: 0.5rem;">';
if (docLink) {
html += `
<a href="${docLink}" target="_blank" class="doc-link" style="margin-right: 1rem;">
View ${displayName} documentation
</a>
`;
}
if (aiContext) {
const sourceGroupIds = this.getAISourceGroupIds(displayName);
html += `
<a href="#" class="ask-ai-open" data-query="Help me with ${aiContext}"${sourceGroupIds ? ` data-source-group-ids="${sourceGroupIds}"` : ''}>
Ask AI about ${displayName}
</a>
`;
}
html += '</div>';
}
}
html += '</div>';
@ -2213,10 +2390,10 @@ docker exec &lt;container&gt; curl -I localhost:8181/ping
</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 class="results-title">Expected results from command:</div>
Response header <strong>X-Influxdb-Build: Enterprise</strong> InfluxDB 3 Enterprise (definitive)<br>
Response header <strong>X-Influxdb-Build: Core</strong> InfluxDB 3 Core (definitive)<br>
Status code <strong>401 Unauthorized</strong> Use the license information below
</div>
<div class="authorization-help">
@ -2261,9 +2438,12 @@ 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 class="results-title">Look for version pattern:</div>
<strong>v1.x.x</strong> (for example, v1.8.10) ${this.getProductDisplayName('oss-v1')}<br>
<strong>v2.x.x</strong> (for example, v2.7.4) ${this.getProductDisplayName('oss-v2')}<br>
<p style="font-size: 0.9em; margin-top: 0.5rem; opacity: 0.8;">
From <code>influxd version</code> command output or <code>X-Influxdb-Version</code> response header
</p>
</div>
<details style="margin: 1rem 0;">
@ -2275,7 +2455,7 @@ curl -I ${url}/ping
docker exec &lt;container&gt; influxd version
# Get ping headers:
docker exec &lt;container&gt; curl -I localhost:8086/ping
docker exec &lt;container&gt; curl -I ${InfluxDBVersionDetector.DEFAULT_HOST_PORT}/ping
# Or check startup logs:
docker logs &lt;container&gt; 2>&1 | head -20

View File

@ -506,16 +506,22 @@
margin-top: var(--spacing-md);
}
// Grafana links styling
.grafana-link {
// Documentation and Ask AI links styling
.doc-link,
.ask-ai-open {
color: $article-link;
text-decoration: underline;
display: inline-block;
&:hover {
color: $article-link-hover;
}
}
.ask-ai-open {
margin-left: 0.5rem;
}
// Manual command output
.manual-output {
margin: 1rem 0;
@ -644,4 +650,4 @@
}
}
}
}

View File

@ -10,6 +10,8 @@ menu:
related:
- /flux/v0/get-started/, Get started with Flux
- https://grafana.com/docs/, Grafana documentation
aliases:
- /enterprise_influxdb/v1/visualize-data/grafana/
alt_links:
core: /influxdb3/core/visualize-data/grafana/
enterprise: /influxdb3/enterprise/visualize-data/grafana/
@ -23,7 +25,7 @@ Use [Grafana](https://grafana.com/) or [Grafana Cloud](https://grafana.com/produ
to visualize data from your **InfluxDB Enterprise** cluster.
> [!Note]
> {{< influxdb-version-detector >}}
> {{< influxdb-version-detector context="grafana" >}}
> [!Note]
> #### Required

View File

@ -10,6 +10,8 @@ menu:
parent: Tools
related:
- /flux/v0/get-started/, Get started with Flux
aliases:
- /influxdb/v1/visualize-data/grafana/
alt_links:
v2: /influxdb/v2/tools/grafana/
core: /influxdb3/core/visualize-data/grafana/
@ -24,7 +26,7 @@ Use [Grafana](https://grafana.com/) or [Grafana Cloud](https://grafana.com/produ
to visualize data from your {{% product-name %}} instance.
> [!Note]
> {{< influxdb-version-detector >}}
> {{< influxdb-version-detector context="grafana" >}}
> [!Note]
> #### Grafana 12.2+

View File

@ -2,7 +2,7 @@ Use [Grafana](https://grafana.com/) or [Grafana Cloud](https://grafana.com/produ
to visualize data from your **InfluxDB {{< current-version >}}** instance.
> [!Note]
> {{< influxdb-version-detector >}}
> {{< influxdb-version-detector context="grafana" >}}
> [!Note]
> #### Grafana 12.2+

View File

@ -2,7 +2,7 @@ Use [Grafana](https://grafana.com/) or [Grafana Cloud](https://grafana.com/produ
to query and visualize data from {{% product-name %}}.
> [!Note]
> {{< influxdb-version-detector >}}
> {{< influxdb-version-detector context="grafana" >}}
> [Grafana] enables you to query, visualize, alert on, and explore your metrics,
> logs, and traces wherever they are stored.

File diff suppressed because it is too large Load Diff

View File

@ -24,5 +24,5 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
// Import custom commands for InfluxDB Version Detector
import './influxdb-version-detector-commands.js';
// Custom commands for InfluxDB Version Detector have been removed
// Tests now use direct Cypress commands for better clarity and maintainability

View File

@ -1,299 +0,0 @@
// Custom Cypress commands for InfluxDB Version Detector testing
/**
* Navigate to a page with the version detector component
* @param {string} [path='/influxdb3/core/visualize-data/grafana/'] - Path to a page with the component
*/
Cypress.Commands.add(
'visitVersionDetector',
(path = '/influxdb3/core/visualize-data/grafana/') => {
cy.visit(path);
cy.get('[data-component="influxdb-version-detector"]', {
timeout: 10000,
}).should('be.visible');
}
);
/**
* Test URL detection for a specific URL
* @param {string} url - The URL to test
* @param {string} expectedProduct - Expected product name in the result
*/
Cypress.Commands.add('testUrlDetection', (url, expectedProduct) => {
cy.get('#influxdb-url').clear().type(url);
cy.get('.submit-button').click();
cy.get('.result.show', { timeout: 5000 }).should('be.visible');
cy.get('.detected-version').should('contain', expectedProduct);
});
/**
* Complete a questionnaire with given answers
* @param {string[]} answers - Array of answers to select in order
*/
Cypress.Commands.add('completeQuestionnaire', (answers) => {
answers.forEach((answer, index) => {
cy.get('.question.active', { timeout: 3000 }).should('be.visible');
cy.get('.option-button').contains(answer).should('be.visible').click();
// Wait for transition between questions
if (index < answers.length - 1) {
cy.wait(500);
}
});
// Wait for final results
cy.get('.result.show', { timeout: 5000 }).should('be.visible');
});
/**
* Start questionnaire with unknown URL
* @param {string} [url='https://unknown-server.com:9999'] - URL to trigger questionnaire
*/
Cypress.Commands.add(
'startQuestionnaire',
(url = 'https://unknown-server.com:9999') => {
cy.get('#influxdb-url').clear().type(url);
cy.get('.submit-button').click();
cy.get('.question.active', { timeout: 5000 }).should('be.visible');
}
);
/**
* Verify questionnaire results contain/don't contain specific products
* @param {Object} options - Configuration object
* @param {string[]} [options.shouldContain] - Products that should be in results
* @param {string[]} [options.shouldNotContain] - Products that should NOT be in results
*/
Cypress.Commands.add(
'verifyQuestionnaireResults',
({ shouldContain = [], shouldNotContain = [] }) => {
cy.get('.result.show', { timeout: 5000 }).should('be.visible');
shouldContain.forEach((product) => {
cy.get('.result').should('contain', product);
});
shouldNotContain.forEach((product) => {
cy.get('.result').should('not.contain', product);
});
}
);
/**
* Test navigation through questionnaire (back/restart functionality)
*/
Cypress.Commands.add('testQuestionnaireNavigation', () => {
// Answer first question
cy.get('.option-button').first().click();
cy.wait(500);
// Test back button
cy.get('.back-button').should('be.visible').click();
cy.get('.question.active').should('be.visible');
cy.get('.progress .progress-bar').should('have.css', 'width', '0px');
// Complete questionnaire to test restart
const quickAnswers = ['Self-hosted', 'Free', '2-5 years', 'SQL'];
cy.completeQuestionnaire(quickAnswers);
// Test restart button
cy.get('.restart-button', { timeout: 3000 }).should('be.visible').click();
cy.get('.question.active').should('be.visible');
cy.get('.progress .progress-bar').should('have.css', 'width', '0px');
});
/**
* Check for JavaScript console errors related to the component
*/
Cypress.Commands.add('checkForConsoleErrors', () => {
cy.window().then((win) => {
const logs = [];
const originalConsoleError = win.console.error;
win.console.error = (...args) => {
logs.push(args.join(' '));
originalConsoleError.apply(win.console, args);
};
// Wait for any potential errors to surface
cy.wait(1000);
cy.then(() => {
const relevantErrors = logs.filter(
(log) =>
log.includes('influxdb-version-detector') ||
log.includes('Failed to parse influxdb_urls data') ||
log.includes('SyntaxError') ||
log.includes('#ZgotmplZ') ||
log.includes('detectContext is not a function')
);
if (relevantErrors.length > 0) {
throw new Error(
`Console errors detected: ${relevantErrors.join('; ')}`
);
}
});
});
});
/**
* Test URL scenarios from the influxdb_urls.yml data
*/
Cypress.Commands.add('testAllKnownUrls', () => {
const urlTestCases = [
// OSS URLs
{ url: 'http://localhost:8086', product: 'InfluxDB OSS' },
{ url: 'https://my-server.com:8086', product: 'InfluxDB OSS' },
// InfluxDB 3 URLs
{ url: 'http://localhost:8181', product: 'InfluxDB 3' },
{ url: 'https://my-server.com:8181', product: 'InfluxDB 3' },
// Cloud URLs
{
url: 'https://us-west-2-1.aws.cloud2.influxdata.com',
product: 'InfluxDB Cloud',
},
{
url: 'https://us-east-1-1.aws.cloud2.influxdata.com',
product: 'InfluxDB Cloud',
},
{
url: 'https://eu-central-1-1.aws.cloud2.influxdata.com',
product: 'InfluxDB Cloud',
},
{
url: 'https://us-central1-1.gcp.cloud2.influxdata.com',
product: 'InfluxDB Cloud',
},
{
url: 'https://westeurope-1.azure.cloud2.influxdata.com',
product: 'InfluxDB Cloud',
},
{
url: 'https://eastus-1.azure.cloud2.influxdata.com',
product: 'InfluxDB Cloud',
},
// Cloud Dedicated
{
url: 'https://cluster-id.a.influxdb.io',
product: 'InfluxDB Cloud Dedicated',
},
{
url: 'https://my-cluster.a.influxdb.io',
product: 'InfluxDB Cloud Dedicated',
},
// Clustered
{ url: 'https://cluster-host.com', product: 'InfluxDB Clustered' },
];
urlTestCases.forEach(({ url, product }) => {
cy.visitVersionDetector();
cy.testUrlDetection(url, product);
});
});
/**
* Test comprehensive questionnaire scenarios
*/
Cypress.Commands.add('testQuestionnaireScenarios', () => {
const scenarios = [
{
name: 'OSS Free User',
answers: ['Self-hosted', 'Free', '2-5 years', 'SQL'],
shouldContain: ['InfluxDB OSS', 'InfluxDB v2'],
shouldNotContain: ['InfluxDB 3 Enterprise'],
},
{
name: 'Cloud Flux User',
answers: [
'Cloud (managed service)',
'Paid',
'Less than 6 months',
'Flux',
],
shouldContain: ['InfluxDB v2'],
shouldNotContain: ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'],
},
{
name: 'Modern Self-hosted SQL User',
answers: ['Self-hosted', 'Paid', 'Less than 6 months', 'SQL'],
shouldContain: ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise'],
shouldNotContain: [],
},
{
name: 'High Volume Enterprise User',
answers: ['Self-hosted', 'Paid', 'Less than 6 months', 'SQL', 'Yes'],
shouldContain: ['InfluxDB 3 Enterprise'],
shouldNotContain: [],
},
{
name: 'Uncertain User',
answers: ["I'm not sure", "I'm not sure", "I'm not sure", "I'm not sure"],
shouldContain: [], // Should still provide some recommendations
shouldNotContain: [],
},
];
scenarios.forEach((scenario) => {
cy.visitVersionDetector();
cy.startQuestionnaire();
cy.completeQuestionnaire(scenario.answers);
cy.verifyQuestionnaireResults({
shouldContain: scenario.shouldContain,
shouldNotContain: scenario.shouldNotContain,
});
});
});
/**
* Test accessibility features
*/
Cypress.Commands.add('testAccessibility', () => {
// Test keyboard navigation
cy.get('body').tab();
cy.focused().should('have.id', 'influxdb-url');
cy.focused().type('https://test.com');
cy.focused().tab();
cy.focused().should('have.class', 'submit-button');
cy.focused().type('{enter}');
cy.get('.question.active', { timeout: 3000 }).should('be.visible');
// Test that buttons are focusable
cy.get('.option-button')
.first()
.should('be.visible')
.focus()
.should('be.focused');
});
/**
* Test theme integration
*/
Cypress.Commands.add('testThemeIntegration', () => {
// Test light theme (default)
cy.get('[data-component="influxdb-version-detector"]')
.should('have.css', 'background-color')
.and('not.equal', 'transparent');
cy.get('.detector-title')
.should('have.css', 'color')
.and('not.equal', 'rgb(0, 0, 0)');
// Test dark theme if theme switcher exists
cy.get('body').then(($body) => {
if ($body.find('[data-theme-toggle]').length > 0) {
cy.get('[data-theme-toggle]').click();
cy.get('[data-component="influxdb-version-detector"]')
.should('have.css', 'background-color')
.and('not.equal', 'rgb(255, 255, 255)');
}
});
});

View File

@ -84,7 +84,15 @@ export default [
'import/no-unresolved': 'off', // Hugo handles module resolution differently
// Code formatting
'max-len': ['warn', { code: 80, ignoreUrls: true, ignoreStrings: true }],
'max-len': [
'warn',
{
code: 80,
ignoreUrls: true,
ignoreStrings: true,
ignoreComments: true,
},
],
quotes: ['error', 'single', { avoidEscape: true }],
// Hugo template string linting (custom rule)
@ -162,7 +170,14 @@ export default [
},
rules: {
// TypeScript-specific rules
'@typescript-eslint/no-unused-vars': 'warn',
'no-unused-vars': 'off', // Disable base rule for TS files
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
},

View File

@ -9,7 +9,7 @@
{{ $detectorProducts := dict }}
{{ range $key, $product := site.Data.products }}
{{ if $product.detector_config }}
{{/* Include detector_config plus name and placeholder_host for configuration guidance */}}
{{/* Include detector_config plus name, placeholder_host, and AI source group IDs */}}
{{ $productData := $product.detector_config }}
{{ if $product.name }}
{{ $productData = merge $productData (dict "name" $product.name) }}
@ -17,6 +17,12 @@
{{ if $product.placeholder_host }}
{{ $productData = merge $productData (dict "placeholder_host" $product.placeholder_host) }}
{{ end }}
{{ if $product.ai_source_group_ids }}
{{ $productData = merge $productData (dict "ai_source_group_ids" $product.ai_source_group_ids) }}
{{ end }}
{{ if $product.ai_source_group_ids__v1 }}
{{ $productData = merge $productData (dict "ai_source_group_ids__v1" $product.ai_source_group_ids__v1) }}
{{ end }}
{{ $detectorProducts = merge $detectorProducts (dict $key $productData) }}
{{ end }}
{{ end }}
@ -25,4 +31,4 @@
<div data-component="influxdb-version-detector"
data-products='{{ $detectorProducts | jsonify }}'
data-influxdb-urls='{{ site.Data.influxdb_urls | jsonify }}'></div>
</div>
</div>