From fe093476172529191e0e0a025cfabb87c51d6e4d Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Fri, 13 Mar 2026 17:28:15 -0600 Subject: [PATCH] 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 --- assets/js/content-interactions.js | 2 +- assets/js/main.js | 2 + assets/js/tc-downloads.js | 221 +++++++++++++++++ assets/styles/layouts/_article.scss | 1 + assets/styles/layouts/_modals.scss | 3 +- .../styles/layouts/article/_tc-downloads.scss | 104 ++++++++ .../styles/layouts/modals/_tc-downloads.scss | 226 ++++++++++++++++++ content/telegraf/controller/install/_index.md | 10 +- cypress/e2e/content/tc-downloads.cy.js | 103 ++++++++ data/tc_downloads.yml | 24 ++ layouts/partials/footer/modals.html | 3 + .../partials/footer/modals/tc-downloads.html | 11 + layouts/shortcodes/telegraf/tc-downloads.html | 17 ++ 13 files changed, 716 insertions(+), 11 deletions(-) create mode 100644 assets/js/tc-downloads.js create mode 100644 assets/styles/layouts/article/_tc-downloads.scss create mode 100644 assets/styles/layouts/modals/_tc-downloads.scss create mode 100644 cypress/e2e/content/tc-downloads.cy.js create mode 100644 data/tc_downloads.yml create mode 100644 layouts/partials/footer/modals/tc-downloads.html create mode 100644 layouts/shortcodes/telegraf/tc-downloads.html diff --git a/assets/js/content-interactions.js b/assets/js/content-interactions.js index eb9b4e1bc..3de1386ea 100644 --- a/assets/js/content-interactions.js +++ b/assets/js/content-interactions.js @@ -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) { diff --git a/assets/js/main.js b/assets/js/main.js index 826ad9a11..abe0792c3 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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(); } /** diff --git a/assets/js/tc-downloads.js b/assets/js/tc-downloads.js new file mode 100644 index 000000000..cc17837d4 --- /dev/null +++ b/assets/js/tc-downloads.js @@ -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 = '
'; + + platforms.forEach((platform) => { + html += `

${platform.name}

`; + html += + '

' + + `Telegraf Controller v${version}` + + '

'; + html += '
'; + + platform.builds.forEach((build) => { + const link = + `${platform.name}` + + ` (${build.arch})`; + const sha = + `sha256:${build.sha256}` + + ''; + html += + '
' + + `
${link}
` + + `
${sha}
` + + '
'; + }); + + html += '
'; + }); + + 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 = ''; + 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 }; diff --git a/assets/styles/layouts/_article.scss b/assets/styles/layouts/_article.scss index d3f56bccd..1b8584fd8 100644 --- a/assets/styles/layouts/_article.scss +++ b/assets/styles/layouts/_article.scss @@ -216,6 +216,7 @@ "article/tabbed-content", "article/tables", "article/tags", + "article/tc-downloads", "article/telegraf-plugins", "article/title", "article/truncate", diff --git a/assets/styles/layouts/_modals.scss b/assets/styles/layouts/_modals.scss index 2a149c378..fadb181eb 100644 --- a/assets/styles/layouts/_modals.scss +++ b/assets/styles/layouts/_modals.scss @@ -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"; } diff --git a/assets/styles/layouts/article/_tc-downloads.scss b/assets/styles/layouts/article/_tc-downloads.scss new file mode 100644 index 000000000..a8b54d3d4 --- /dev/null +++ b/assets/styles/layouts/article/_tc-downloads.scss @@ -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; + } + } +} diff --git a/assets/styles/layouts/modals/_tc-downloads.scss b/assets/styles/layouts/modals/_tc-downloads.scss new file mode 100644 index 000000000..a9def5ae8 --- /dev/null +++ b/assets/styles/layouts/modals/_tc-downloads.scss @@ -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; + } + } +} diff --git a/content/telegraf/controller/install/_index.md b/content/telegraf/controller/install/_index.md index a58650213..908ec8fc3 100644 --- a/content/telegraf/controller/install/_index.md +++ b/content/telegraf/controller/install/_index.md @@ -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 %}}**. diff --git a/cypress/e2e/content/tc-downloads.cy.js b/cypress/e2e/content/tc-downloads.cy.js new file mode 100644 index 000000000..c7d8045c1 --- /dev/null +++ b/cypress/e2e/content/tc-downloads.cy.js @@ -0,0 +1,103 @@ +/// + +/** + * 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; + }); + }); + }); +}); diff --git a/data/tc_downloads.yml b/data/tc_downloads.yml new file mode 100644 index 000000000..834774eb7 --- /dev/null +++ b/data/tc_downloads.yml @@ -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" diff --git a/layouts/partials/footer/modals.html b/layouts/partials/footer/modals.html index 9830135dd..56f27291f 100644 --- a/layouts/partials/footer/modals.html +++ b/layouts/partials/footer/modals.html @@ -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 }}
\ No newline at end of file diff --git a/layouts/partials/footer/modals/tc-downloads.html b/layouts/partials/footer/modals/tc-downloads.html new file mode 100644 index 000000000..882c25545 --- /dev/null +++ b/layouts/partials/footer/modals/tc-downloads.html @@ -0,0 +1,11 @@ + diff --git a/layouts/shortcodes/telegraf/tc-downloads.html b/layouts/shortcodes/telegraf/tc-downloads.html new file mode 100644 index 000000000..ceae8efa8 --- /dev/null +++ b/layouts/shortcodes/telegraf/tc-downloads.html @@ -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. +*/}} +