feat(tc-download): add gated download for telegraf controller (#6940)

* feat(tc-download): add gated download for telegraf controller

* test(tc-download): add Cypress E2E tests for gated downloads

Cover four key behaviors: gated state (no localStorage key), ungated
state (key present), query param flow (?ref=tc), and SHA copy buttons.

---------

Co-authored-by: Jason Stirnaman <jstirnaman@influxdata.com>
tc-release-notes
Scott Anderson 2026-03-13 17:28:15 -06:00 committed by GitHub
parent 568f9f3732
commit fe09347617
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 716 additions and 11 deletions

View File

@ -122,7 +122,7 @@ function expandAccordions() {
// Expand accordions on load based on URL anchor
function openAccordionByHash() {
var anchor = window.location.hash;
var anchor = window.location.hash.split('?')[0];
function expandElement() {
if ($(anchor).parents('.expand').length > 0) {

View File

@ -19,6 +19,7 @@ import * as pageContext from './page-context.js';
import * as pageFeedback from './page-feedback.js';
import * as tabbedContent from './tabbed-content.js';
import * as v3Wayfinding from './v3-wayfinding.js';
import * as tcDownloads from './tc-downloads.js';
/** Import component modules
* The component pattern organizes JavaScript, CSS, and HTML for a specific UI element or interaction:
@ -162,6 +163,7 @@ function initModules() {
pageFeedback.initialize();
tabbedContent.initialize();
v3Wayfinding.initialize();
tcDownloads.initialize();
}
/**

221
assets/js/tc-downloads.js Normal file
View File

@ -0,0 +1,221 @@
////////////////////////////////////////////////////////////////////////////////
///////////////// Telegraf Controller gated downloads module ////////////////////
////////////////////////////////////////////////////////////////////////////////
import { toggleModal } from './modals.js';
const STORAGE_KEY = 'influxdata_docs_tc_dl';
const QUERY_PARAM = 'ref';
const QUERY_VALUE = 'tc';
// ─── localStorage helpers ───────────────────────────────────────────────────
function setDownloadKey() {
localStorage.setItem(STORAGE_KEY, 'true');
}
function hasDownloadKey() {
return localStorage.getItem(STORAGE_KEY) === 'true';
}
// ─── Query param helpers ────────────────────────────────────────────────────
function hasRefParam() {
// Check query string first (?ref=tc before the hash)
const params = new URLSearchParams(window.location.search);
if (params.get(QUERY_PARAM) === QUERY_VALUE) return true;
// Also check inside the fragment (#heading?ref=tc)
const hash = window.location.hash;
const qIndex = hash.indexOf('?');
if (qIndex !== -1) {
const hashParams = new URLSearchParams(hash.substring(qIndex));
if (hashParams.get(QUERY_PARAM) === QUERY_VALUE) return true;
}
return false;
}
function stripRefParam() {
const url = new URL(window.location.href);
// Remove from query string
url.searchParams.delete(QUERY_PARAM);
// Remove from fragment if present (#heading?ref=tc → #heading)
let hash = url.hash;
const qIndex = hash.indexOf('?');
if (qIndex !== -1) {
const hashBase = hash.substring(0, qIndex);
const hashParams = new URLSearchParams(hash.substring(qIndex));
hashParams.delete(QUERY_PARAM);
const remaining = hashParams.toString();
hash = remaining ? `${hashBase}?${remaining}` : hashBase;
}
window.history.replaceState({}, '', url.pathname + url.search + hash);
}
// ─── Download link rendering ────────────────────────────────────────────────
function renderDownloadLinks(container, data) {
const version = data.version;
const platforms = data.platforms;
let html = '<div class="tc-downloads-container">';
platforms.forEach((platform) => {
html += `<h3>${platform.name}</h3>`;
html +=
'<p class="tc-version">' +
`<em>Telegraf Controller v${version}</em>` +
'</p>';
html += '<div class="tc-build-table">';
platform.builds.forEach((build) => {
const link =
`<a href="${build.url}"` +
' class="btn tc-download-link download"' +
` download>${platform.name}` +
` (${build.arch})</a>`;
const sha =
`<code>sha256:${build.sha256}</code>` +
'<button class="tc-copy-sha"' +
` data-sha="${build.sha256}">` +
'&#59693;</button>';
html +=
'<div class="tc-build-row">' +
`<div class="tc-build-download">${link}</div>` +
`<div class="tc-build-sha">${sha}</div>` +
'</div>';
});
html += '</div>';
});
container.innerHTML = html;
}
// ─── Clipboard copy ─────────────────────────────────────────────────────────
function copyToClipboard(sha, button) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(sha).then(() => {
showCopiedFeedback(button);
});
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = sha;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showCopiedFeedback(button);
}
}
function showCopiedFeedback(button) {
const original = button.innerHTML;
button.innerHTML = '&#59671;';
setTimeout(() => {
button.innerHTML = original;
}, 2000);
}
// ─── Marketo form ───────────────────────────────────────────────────────────
function initMarketoForm() {
/* global MktoForms2 */
if (typeof MktoForms2 === 'undefined') {
console.error('tc-downloads: MktoForms2 not loaded');
return;
}
MktoForms2.setOptions({
formXDPath: '/rs/972-GDU-533/images/marketo-xdframe-relative.html',
});
MktoForms2.loadForm(
'https://get.influxdata.com',
'972-GDU-533',
3195,
function (form) {
form.addHiddenFields({ mkto_content_name: 'Telegraf Enterprise Alpha' });
form.onSuccess(function () {
setDownloadKey();
toggleModal();
// Redirect to self with ?ref=tc to trigger downloads on reload
const url = new URL(window.location.href);
url.searchParams.set(QUERY_PARAM, QUERY_VALUE);
window.location.href = url.toString();
// Prevent Marketo's default redirect
return false;
});
}
);
}
// ─── View state management ──────────────────────────────────────────────────
function showDownloads(area) {
const btn = area.querySelector('#tc-download-btn');
const linksContainer = area.querySelector('#tc-downloads-links');
if (!linksContainer) return;
// Parse download data from the JSON data attribute
const rawData = linksContainer.getAttribute('data-downloads');
if (!rawData) return;
let data;
try {
data = JSON.parse(atob(rawData));
} catch (e) {
console.error('tc-downloads: failed to parse download data', e);
return;
}
// Hide the download button
if (btn) btn.style.display = 'none';
// Render download links and show the container
renderDownloadLinks(linksContainer, data);
linksContainer.style.display = 'block';
}
// ─── Initialize ─────────────────────────────────────────────────────────────
function initialize() {
// 1. Handle ?ref=tc query param on any page
if (hasRefParam()) {
setDownloadKey();
stripRefParam();
}
const area = document.getElementById('tc-downloads-area');
if (!area) return; // No shortcode on this page — no-op
// 2. Check localStorage and show appropriate view
if (hasDownloadKey()) {
showDownloads(area);
}
// 3. Initialize Marketo form
initMarketoForm();
// 4. Delegated click handler for SHA copy buttons
area.addEventListener('click', function (e) {
const copyBtn = e.target.closest('.tc-copy-sha');
if (copyBtn) {
const sha = copyBtn.getAttribute('data-sha');
if (sha) copyToClipboard(sha, copyBtn);
}
});
}
export { initialize };

View File

@ -216,6 +216,7 @@
"article/tabbed-content",
"article/tables",
"article/tags",
"article/tc-downloads",
"article/telegraf-plugins",
"article/title",
"article/truncate",

View File

@ -135,7 +135,8 @@
@import "modals/url-selector";
@import "modals/page-feedback";
@import "modals/flux-versions";
@import "modals/_influxdb-gs-datepicker"
@import "modals/_influxdb-gs-datepicker";
@import "modals/tc-downloads";
}

View File

@ -0,0 +1,104 @@
/////////////////// Styles for inline TC download links ////////////////////////
#tc-downloads-area {
margin: 0 0 2rem;
#tc-download-btn {
display: inline-block;
}
.tc-version {
font-size: 1rem;
color: rgba($article-text, .6);
margin-bottom: .5rem;
}
.tc-build-table {
margin-bottom: 1rem;
}
.tc-build-row {
display: flex;
align-items: center;
border-bottom: 1px solid $article-hr;
&:first-child {
border-top: 1px solid $article-hr;
}
}
.tc-build-download {
flex: 1 1 auto;
margin-right: 1rem;
}
.tc-download-link {
font-size: 1rem;
padding: .35rem 1rem;
white-space: nowrap;
}
.tc-build-sha {
flex: 1 1 auto;
display: flex;
justify-content: flex-end;
gap: .1rem;
min-width: 0;
max-width: 25rem;
code {
font-size: .8rem;
padding: .15rem .65rem;
color: $article-code;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tc-copy-sha {
flex-shrink: 0;
background: $article-code-bg;
border: none;
border-radius: $radius;
padding: .2rem .6rem;
font-family: 'icomoon-v4';
font-size: .9rem;
color: rgba($article-code, .85);
cursor: pointer;
transition: color .2s;
&:hover {
color: $article-code-link-hover;
}
}
}
}
//////////////////////////////// MEDIA QUERIES /////////////////////////////////
@include media(small) {
#tc-downloads-area {
.tc-build-row {
flex-direction: column;
align-items: flex-start;
gap: .5rem;
}
.tc-build-download {
margin-right: 0;
width: 100%;
}
.tc-download-link {
width: 100%;
text-align: center;
}
.tc-build-sha {
width: 100%;
max-width: 100%;
margin-bottom: .5rem;
}
}
}

View File

@ -0,0 +1,226 @@
////////////////////// Styles for the TC downloads modal ////////////////////////
#tc-downloads {
// Reset Marketo's inline styles and defaults ────────────────────────────
.mktoForm {
width: 100% !important;
font-family: $proxima !important;
font-size: 1rem !important;
color: $article-text !important;
padding: 0 !important;
}
// Hide Marketo's offset/gutter spacers
.mktoOffset,
.mktoGutter {
display: none !important;
}
// Form layout: 2-column grid for first 4 fields
.mktoForm {
display: grid !important;
grid-template-columns: 1fr 1fr;
gap: 0 1.75rem;
}
// Visible field rows (First Name, Last Name, Company, Job Title)
// occupy one grid cell each pairs share a row automatically
.mktoFormRow {
margin-bottom: .5rem;
}
// Hidden field rows collapse they don't disrupt the grid
.mktoFormRow:has(input[type='hidden']) {
display: none;
}
// Email, Privacy, and Submit span full width
.mktoFormRow:has(.mktoEmailField),
.mktoFormRow:has(.mktoCheckboxList),
.mktoButtonRow {
grid-column: 1 / -1;
}
.mktoFieldDescriptor,
.mktoFieldWrap {
width: 100% !important;
margin-bottom: 0 !important;
}
// Labels
.mktoLabel {
display: flex !important;
align-items: baseline;
width: 100% !important;
font-family: $proxima !important;
font-weight: $medium !important;
font-size: .9rem !important;
color: $article-bold !important;
padding: .5rem 0 .1rem !important;
}
.mktoAsterix {
order: 1;
color: #e85b5b !important;
float: none !important;
padding-left: .15rem;
}
// Text inputs
.mktoField.mktoTextField,
.mktoField.mktoEmailField {
width: 100% !important;
font-family: $proxima !important;
font-weight: $medium !important;
font-size: 1rem !important;
background: rgba($article-text, .06) !important;
border-radius: $radius !important;
border: 1px solid rgba($article-text, .06) !important;
padding: .5em !important;
color: $article-text !important;
transition-property: border;
transition-duration: .2s;
box-shadow: none !important;
&:focus {
outline: none !important;
border-color: $sidebar-search-highlight !important;
}
&::placeholder {
color: rgba($sidebar-search-text, .45) !important;
font-weight: normal !important;
font-style: italic !important;
}
}
// Checkbox / privacy consent
.mktoFormRow:has(.mktoCheckboxList) .mktoAsterix {
display: none !important;
}
.mktoCheckboxList {
width: 100% !important;
label {
font-family: $proxima !important;
font-size: .85rem !important;
line-height: 1.4 !important;
color: rgba($article-text, .7) !important;
&::after {
content: '*';
color: #e85b5b;
font-weight: $medium;
font-size: .95rem;
font-style: normal;
}
a {
color: $article-link !important;
font-weight: $medium;
text-decoration: none;
transition: color .2s;
&:hover {
color: $article-link-hover !important;
}
}
}
input[type='checkbox'] {
margin: .2rem .65rem 0 0;
}
}
// Submit button
.mktoButtonRow {
margin-top: 1rem;
display: flex;
justify-content: flex-end;
}
.mktoButtonWrap {
margin-left: 0 !important;
}
.mktoButton {
@include gradient($article-btn-gradient);
border: none !important;
border-radius: $radius !important;
padding: .65rem 1.5rem !important;
font-family: $proxima !important;
font-weight: $medium !important;
font-size: 1rem !important;
color: $g20-white !important;
cursor: pointer;
transition: opacity .2s;
&:hover {
@include gradient($article-btn-gradient-hover);
}
}
// Validation errors
// Marketo positions errors absolutely make them flow inline instead
.mktoFieldWrap {
position: relative;
}
.mktoError {
position: relative !important;
bottom: auto !important;
left: auto !important;
right: auto !important;
pointer-events: none;
.mktoErrorArrow {
display: none !important;
}
.mktoErrorMsg {
font-family: $proxima !important;
font-size: .8rem !important;
max-width: 100% !important;
background: none !important;
border: none !important;
color: #e85b5b !important;
padding: .15rem 0 0 !important;
box-shadow: none !important;
text-shadow: none !important;
}
}
// Custom error message
.tc-form-error {
margin: .75rem 0;
padding: .5rem .75rem;
background: rgba(#e85b5b, .1);
border: 1px solid rgba(#e85b5b, .3);
border-radius: $radius;
color: #e85b5b;
font-size: .9rem;
}
// Clear floats
.mktoClear {
clear: both;
}
}
//////////////////////////////// MEDIA QUERIES /////////////////////////////////
@include media(small) {
#tc-downloads {
.mktoForm {
grid-template-columns: 1fr;
}
.mktoFormRow:has(.mktoEmailField),
.mktoFormRow:has(.mktoCheckboxList),
.mktoButtonRow {
grid-column: auto;
}
}
}

View File

@ -76,15 +76,7 @@ $env:TELEGRAF_CONTROLLER_EULA="accept"
1. **Download the {{% product-name %}} executable.**
> [!Note]
> #### Contact InfluxData for download
>
> If you are currently participating in the {{% product-name %}} private alpha,
> send your operating system and architecture to InfluxData and we will
> provide you with the appropriate {{% product-name %}} executable.
>
> If you are not currently in the private alpha and would like to be,
> [request early access](https://www.influxdata.com/products/telegraf-enterprise).
{{< telegraf/tc-downloads >}}
2. **Install {{% product-name %}}**.

View File

@ -0,0 +1,103 @@
/// <reference types="cypress" />
/**
* E2E tests for the Telegraf Controller gated downloads module (tc-downloads.js).
*
* Tests the four key user-facing behaviors:
* 1. Gated state no localStorage key button visible, links hidden
* 2. Ungated state localStorage key present links rendered, button hidden
* 3. Query param ?ref=tc visit key set, downloads shown
* 4. SHA copy button present when downloads are rendered
*
* Marketo form submission is NOT tested (external dependency).
*/
const PAGE_URL = '/telegraf/controller/install/';
const STORAGE_KEY = 'influxdata_docs_tc_dl';
describe('Telegraf Controller gated downloads', () => {
describe('Gated state (no localStorage key)', () => {
beforeEach(() => {
// Clear any existing key so the page starts in the gated state.
cy.clearLocalStorage();
cy.visit(PAGE_URL);
});
it('shows the download button', () => {
cy.get('#tc-download-btn').should('be.visible');
});
it('keeps the download links container hidden', () => {
cy.get('#tc-downloads-links').should('not.be.visible');
});
it('does not render download link anchors', () => {
cy.get('.tc-download-link').should('not.exist');
});
});
describe('Ungated state (localStorage key present)', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit(PAGE_URL, {
onBeforeLoad(win) {
win.localStorage.setItem(STORAGE_KEY, 'true');
},
});
});
it('hides the download button', () => {
cy.get('#tc-download-btn').should('not.be.visible');
});
it('shows the downloads container', () => {
cy.get('#tc-downloads-links').should('be.visible');
});
it('renders at least one download link', () => {
cy.get('.tc-download-link').should('have.length.at.least', 1);
});
it('renders SHA copy buttons for each build', () => {
cy.get('.tc-copy-sha').should('have.length.at.least', 1);
});
});
describe('Query param flow (?ref=tc)', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit(`${PAGE_URL}?ref=tc`);
});
it('sets the localStorage key', () => {
cy.window().then((win) => {
expect(win.localStorage.getItem(STORAGE_KEY)).to.equal('true');
});
});
it('shows download links after the param is processed', () => {
cy.get('.tc-download-link').should('have.length.at.least', 1);
});
it('strips the ?ref=tc param from the URL', () => {
cy.url().should('not.include', 'ref=tc');
});
});
describe('SHA copy button', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit(PAGE_URL, {
onBeforeLoad(win) {
win.localStorage.setItem(STORAGE_KEY, 'true');
},
});
});
it('each copy button carries a data-sha attribute', () => {
cy.get('.tc-copy-sha').each(($btn) => {
expect($btn.attr('data-sha')).to.be.a('string').and.not.be.empty;
});
});
});
});

24
data/tc_downloads.yml Normal file
View File

@ -0,0 +1,24 @@
version: "0.0.5-beta"
platforms:
- name: Linux
builds:
- arch: x86_64
filename: telegraf-controller-1.0.0_linux_x86_64.tar.gz
url: https://dl.influxdata.com/telegraf-controller/releases/telegraf-controller-1.0.0_linux_x86_64.tar.gz
sha256: "placeholder"
- name: macOS
builds:
- arch: arm64
filename: telegraf-controller-1.0.0_darwin_arm64.tar.gz
url: https://dl.influxdata.com/telegraf-controller/releases/telegraf-controller-1.0.0_darwin_arm64.tar.gz
sha256: "placeholder"
- arch: x86_64
filename: telegraf-controller-1.0.0_darwin_x86_64.tar.gz
url: https://dl.influxdata.com/telegraf-controller/releases/telegraf-controller-1.0.0_darwin_x86_64.tar.gz
sha256: "placeholder"
- name: Windows
builds:
- arch: x86_64
filename: telegraf-controller-1.0.0_windows_x86_64.zip
url: https://dl.influxdata.com/telegraf-controller/releases/telegraf-controller-1.0.0_windows_x86_64.zip
sha256: "placeholder"

View File

@ -14,6 +14,9 @@
{{ if $inStdlib }}
{{ partial "footer/modals/flux-influxdb-versions.html" . }}
{{ end }}
{{ if .Page.HasShortcode "telegraf/tc-downloads" }}
{{ partial "footer/modals/tc-downloads.html" . }}
{{ end }}
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="modal-content" id="tc-downloads">
<h3>Download Telegraf Controller</h3>
<p>Provide your information to access Telegraf Controller downloads.</p>
<script src="https://get.influxdata.com/js/forms2/js/forms2.min.js"></script>
<form id="mktoForm_3195"></form>
<div class="tc-form-error" style="display: none;">
An error occurred. Please try again.
</div>
</div>

View File

@ -0,0 +1,17 @@
{{/*
tc-downloads shortcode
Renders a gated download experience for Telegraf Controller.
- Shows a "Download" button that opens a contact form modal.
- After form submission (or email link with ?ref=tc), JS renders
download links from the JSON data attribute.
- Data sourced from data/tc_downloads.yml.
*/}}
<div id="tc-downloads-area">
<a class="btn" id="tc-download-btn"
onclick="window.influxdatadocs.toggleModal('#tc-downloads')">
Download Telegraf Controller
</a>
<div id="tc-downloads-links" style="display: none;"
data-downloads="{{ .Site.Data.tc_downloads | jsonify | base64Encode }}">
</div>
</div>