Merge branch 'master' into feat-oss-2.7.12-release

pull/6070/head
Jason Stirnaman 2025-05-19 14:58:40 -05:00 committed by GitHub
commit f8f48d451a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 3759 additions and 780 deletions

View File

@ -31,10 +31,10 @@ jobs:
command: cd api-docs && bash generate-api-docs.sh
- run:
name: Inject Flux stdlib frontmatter
command: node ./flux-build-scripts/inject-flux-stdlib-frontmatter.js
command: node ./flux-build-scripts/inject-flux-stdlib-frontmatter.cjs
- run:
name: Update Flux/InfluxDB versions
command: node ./flux-build-scripts/update-flux-versions.js
command: node ./flux-build-scripts/update-flux-versions.cjs
- save_cache:
key: install-{{ .Environment.CACHE_VERSION }}-{{ checksum ".circleci/config.yml" }}
paths:

2
.gitignore vendored
View File

@ -16,6 +16,8 @@ node_modules
!telegraf-build/scripts
!telegraf-build/README.md
/cypress/screenshots/*
/cypress/videos/*
test-results.xml
/influxdb3cli-build-scripts/content
.vscode/*
.idea

57
.husky/_/serve Executable file
View File

@ -0,0 +1,57 @@
#!/bin/sh
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
set -x
fi
if [ "$LEFTHOOK" = "0" ]; then
exit 0
fi
call_lefthook()
{
if test -n "$LEFTHOOK_BIN"
then
"$LEFTHOOK_BIN" "$@"
elif lefthook -h >/dev/null 2>&1
then
lefthook "$@"
else
dir="$(git rev-parse --show-toplevel)"
osArch=$(uname | tr '[:upper:]' '[:lower:]')
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook"
then
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook"
then
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook"
then
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@"
elif test -f "$dir/node_modules/lefthook/bin/index.js"
then
"$dir/node_modules/lefthook/bin/index.js" "$@"
elif bundle exec lefthook -h >/dev/null 2>&1
then
bundle exec lefthook "$@"
elif yarn lefthook -h >/dev/null 2>&1
then
yarn lefthook "$@"
elif pnpm lefthook -h >/dev/null 2>&1
then
pnpm lefthook "$@"
elif swift package plugin lefthook >/dev/null 2>&1
then
swift package --disable-sandbox plugin lefthook "$@"
elif command -v mint >/dev/null 2>&1
then
mint run csjones/lefthook-plugin "$@"
else
echo "Can't find lefthook in PATH"
fi
fi
}
call_lefthook run "serve" "$@"

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v23.10.0

View File

@ -28,8 +28,10 @@ For the linting and tests to run, you need to install Docker and Node.js
dependencies.
\_**Note:**
We strongly recommend running linting and tests, but you can skip them
(and avoid installing dependencies)
The git pre-commit and pre-push hooks are configured to run linting and tests automatically
when you commit or push changes.
We strongly recommend letting them run, but you can skip them
(and avoid installing related dependencies)
by including the `--no-verify` flag with your commit--for example, enter the following command in your terminal:
```sh
@ -51,7 +53,7 @@ dev dependencies used in pre-commit hooks for linting, syntax-checking, and test
Dev dependencies include:
- [Lefthook](https://github.com/evilmartians/lefthook): configures and
manages pre-commit hooks for linting and testing Markdown content.
manages git pre-commit and pre-push hooks for linting and testing Markdown content.
- [prettier](https://prettier.io/docs/en/): formats code, including Markdown, according to style rules for consistency
- [Cypress]: e2e testing for UI elements and URLs in content
@ -93,9 +95,11 @@ Make your suggested changes being sure to follow the [style and formatting guide
## Lint and test your changes
`package.json` contains scripts for running tests and linting.
### Automatic pre-commit checks
docs-v2 uses Lefthook to manage Git hooks, such as pre-commit hooks that lint Markdown and test code blocks.
docs-v2 uses Lefthook to manage Git hooks that run during pre-commit and pre-push. The hooks run the scripts defined in `package.json` to lint Markdown and test code blocks.
When you try to commit changes (`git commit`), Git runs
the commands configured in `lefthook.yml` which pass your **staged** files to Vale,
Prettier, Cypress (for UI tests and link-checking), and Pytest (for testing Python and shell code in code blocks).

View File

@ -1,5 +1,5 @@
plugins:
- './../openapi/plugins/docs-plugin.js'
- './../openapi/plugins/docs-plugin.cjs'
extends:
- recommended
- docs/all

View File

@ -1,5 +1,5 @@
plugins:
- '../../openapi/plugins/docs-plugin.js'
- '../../openapi/plugins/docs-plugin.cjs'
extends:
- recommended
- docs/all

View File

@ -1,5 +1,5 @@
plugins:
- '../../openapi/plugins/docs-plugin.js'
- '../../openapi/plugins/docs-plugin.cjs'
extends:
- recommended
- docs/all

View File

@ -1,5 +1,5 @@
plugins:
- '../../openapi/plugins/docs-plugin.js'
- '../../openapi/plugins/docs-plugin.cjs'
extends:
- recommended
- docs/all

View File

@ -1,5 +1,5 @@
plugins:
- '../../openapi/plugins/docs-plugin.js'
- '../../openapi/plugins/docs-plugin.cjs'
extends:
- recommended
- docs/all

View File

@ -1,5 +1,5 @@
plugins:
- '../../openapi/plugins/docs-plugin.js'
- '../../openapi/plugins/docs-plugin.cjs'
extends:
- recommended
- docs/all

View File

@ -1,5 +1,5 @@
plugins:
- '../../openapi/plugins/docs-plugin.js'
- '../../openapi/plugins/docs-plugin.cjs'
extends:
- recommended
- docs/all

View File

@ -1,5 +1,5 @@
plugins:
- '../../openapi/plugins/docs-plugin.js'
- '../../openapi/plugins/docs-plugin.cjs'
extends:
- recommended
- docs/all

View File

@ -1,6 +1,6 @@
module.exports = SetTagGroups;
const { collect, getName, sortName, isPresent } = require('../../helpers/content-helper.js')
const { collect, getName, sortName, isPresent } = require('../../helpers/content-helper.cjs')
/**
* Returns an object that defines handler functions for:
* - Operation nodes

View File

@ -1,25 +0,0 @@
module.exports = SetTags;
const { tags } = require('../../../content/content')
/**
* Returns an object that defines handler functions for:
* - DefinitionRoot (the root openapi) node
* The DefinitionRoot handler, executed when
* the parser is leaving the root node,
* sets the root `tags` list to the provided `data`.
*/
/** @type {import('@redocly/openapi-cli').OasDecorator} */
function SetTags() {
const data = tags();
return {
DefinitionRoot: {
/** Set tags from custom tags when visitor enters root. */
enter(root) {
if(data) {
root.tags = data;
}
}
}
}
};

View File

@ -1,5 +1,5 @@
const path = require('path');
const { toJSON } = require('./helpers/content-helper');
const { toJSON } = require('./helpers/content-helper.cjs');
function getVersioned(filename) {
const apiDocsRoot=path.resolve(process.env.API_DOCS_ROOT_PATH || process.cwd());

View File

@ -1,14 +1,14 @@
const {info, servers, tagGroups} = require('./docs-content');
const ReportTags = require('./rules/report-tags');
const ValidateServersUrl = require('./rules/validate-servers-url');
const RemovePrivatePaths = require('./decorators/paths/remove-private-paths');
const ReplaceShortcodes = require('./decorators/replace-shortcodes');
const SetInfo = require('./decorators/set-info');
const DeleteServers = require('./decorators/servers/delete-servers');
const SetServers = require('./decorators/servers/set-servers');
const SetTagGroups = require('./decorators/tags/set-tag-groups');
const StripVersionPrefix = require('./decorators/paths/strip-version-prefix');
const StripTrailingSlash = require('./decorators/paths/strip-trailing-slash');
const {info, servers, tagGroups} = require('./docs-content.cjs');
const ReportTags = require('./rules/report-tags.cjs');
const ValidateServersUrl = require('./rules/validate-servers-url.cjs');
const RemovePrivatePaths = require('./decorators/paths/remove-private-paths.cjs');
const ReplaceShortcodes = require('./decorators/replace-shortcodes.cjs');
const SetInfo = require('./decorators/set-info.cjs');
const DeleteServers = require('./decorators/servers/delete-servers.cjs');
const SetServers = require('./decorators/servers/set-servers.cjs');
const SetTagGroups = require('./decorators/tags/set-tag-groups.cjs');
const StripVersionPrefix = require('./decorators/paths/strip-version-prefix.cjs');
const StripTrailingSlash = require('./decorators/paths/strip-trailing-slash.cjs');
const id = 'docs';

1
assets/js/index.js Normal file
View File

@ -0,0 +1 @@
export * from './main.js';

View File

@ -6,9 +6,6 @@
/** Import modules that are not components.
* TODO: Refactor these into single-purpose component modules.
*/
// import * as codeblocksPreferences from './api-libs.js';
// import * as datetime from './datetime.js';
// import * as featureCallouts from './feature-callouts.js';
import * as apiLibs from './api-libs.js';
import * as codeControls from './code-controls.js';
import * as contentInteractions from './content-interactions.js';
@ -21,15 +18,6 @@ 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 homeInteractions from './home-interactions.js';
// import { getUrls, getReferrerHost, InfluxDBUrl } from './influxdb-url.js';
// import * as keybindings from './keybindings.js';
// import * as listFilters from './list-filters.js';
// import { Modal } from './modal.js';
// import { showNotifications } from './notifications.js';
// import ReleaseTOC from './release-toc.js';
// import * as scroll from './scroll.js';
// import { TabbedContent } from './tabbed-content.js';
/** Import component modules
* The component pattern organizes JavaScript, CSS, and HTML for a specific UI element or interaction:
@ -41,40 +29,95 @@ import * as v3Wayfinding from './v3-wayfinding.js';
import AskAITrigger from './ask-ai-trigger.js';
import CodePlaceholder from './code-placeholders.js';
import { CustomTimeTrigger } from './custom-timestamps.js';
import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js';
import { SearchButton } from './search-button.js';
import { SidebarToggle } from './sidebar-toggle.js';
import Theme from './theme.js';
import ThemeSwitch from './theme-switch.js';
// import CodeControls from './code-controls.js';
// import ContentInteractions from './content-interactions.js';
// import CustomTimestamps from './custom-timestamps.js';
// import Diagram from './Diagram.js';
// import FluxGroupKeysExample from './FluxGroupKeysExample.js';
import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js';
// import PageFeedback from './page-feedback.js';
// import SearchInput from './SearchInput.js';
// import Sidebar from './Sidebar.js';
// import V3Wayfinding from './v3-wayfinding.js';
// import VersionSelector from './VersionSelector.js';
// Expose libraries and components within a namespaced object (for backwards compatibility or testing)
// Expose libraries and components within a namespaced object (for backwards compatibility or testing)
/**
* Component Registry
* A central registry that maps component names to their constructor functions.
* Add new components to this registry as they are created or migrated from non-component modules.
* This allows for:
* 1. Automatic component initialization based on data-component attributes
* 2. Centralized component management
* 3. Easy addition/removal of components
* 4. Simplified testing and debugging
*/
const componentRegistry = {
'ask-ai-trigger': AskAITrigger,
'code-placeholder': CodePlaceholder,
'custom-time-trigger': CustomTimeTrigger,
'flux-influxdb-versions-trigger': FluxInfluxDBVersionsTrigger,
'search-button': SearchButton,
'sidebar-toggle': SidebarToggle,
'theme': Theme,
'theme-switch': ThemeSwitch
};
document.addEventListener('DOMContentLoaded', function () {
/**
* Initialize global namespace for documentation JavaScript
* Exposes core modules for debugging, testing, and backwards compatibility
*/
function initGlobals() {
if (typeof window.influxdatadocs === 'undefined') {
window.influxdatadocs = {};
}
// Expose modules to the global object for debugging, testing, and backwards compatibility for non-ES6 modules.
// Expose modules to the global object for debugging, testing, and backwards compatibility
window.influxdatadocs.delay = delay;
window.influxdatadocs.localStorage = window.LocalStorageAPI = localStorage;
window.influxdatadocs.pageContext = pageContext;
window.influxdatadocs.toggleModal = modals.toggleModal;
window.influxdatadocs.componentRegistry = componentRegistry;
return window.influxdatadocs;
}
// On content loaded, initialize (not-component-ready) UI interaction modules
// To differentiate these from component-ready modules, these modules typically export an initialize function that wraps UI interactions and event listeners.
/**
* Initialize components based on data-component attributes
* @param {Object} globals - The global influxdatadocs namespace
*/
function initComponents(globals) {
const components = document.querySelectorAll('[data-component]');
components.forEach((component) => {
const componentName = component.getAttribute('data-component');
const ComponentConstructor = componentRegistry[componentName];
if (ComponentConstructor) {
// Initialize the component and store its instance in the global namespace
try {
const instance = ComponentConstructor({ component });
globals[componentName] = ComponentConstructor;
// Optionally store component instances for future reference
if (!globals.instances) {
globals.instances = {};
}
if (!globals.instances[componentName]) {
globals.instances[componentName] = [];
}
globals.instances[componentName].push({
element: component,
instance
});
} catch (error) {
console.error(`Error initializing component "${componentName}":`, error);
}
} else {
console.warn(`Unknown component: "${componentName}"`);
}
});
}
/**
* Initialize all non-component modules
*/
function initModules() {
modals.initialize();
apiLibs.initialize();
codeControls.initialize();
@ -84,67 +127,24 @@ document.addEventListener('DOMContentLoaded', function () {
pageFeedback.initialize();
tabbedContent.initialize();
v3Wayfinding.initialize();
}
/** Initialize components
Component Structure: Each component is structured as a jQuery anonymous function that listens for the document ready state.
Initialization in main.js: Each component is called in main.js inside a jQuery document ready function to ensure they are initialized when the document is ready.
Note: These components should *not* be called directly in the HTML.
*/
const components = document.querySelectorAll('[data-component]');
components.forEach((component) => {
const componentName = component.getAttribute('data-component');
switch (componentName) {
case 'ask-ai-trigger':
AskAITrigger({ component });
window.influxdatadocs[componentName] = AskAITrigger;
break;
case 'code-placeholder':
CodePlaceholder({ component });
window.influxdatadocs[componentName] = CodePlaceholder;
break;
case 'custom-time-trigger':
CustomTimeTrigger({ component });
window.influxdatadocs[componentName] = CustomTimeTrigger;
break;
case 'flux-influxdb-versions-trigger':
FluxInfluxDBVersionsTrigger({ component });
window.influxdatadocs[componentName] = FluxInfluxDBVersionsTrigger;
break;
case 'search-button':
SearchButton({ component });
window.influxdatadocs[componentName] = SearchButton;
break;
case 'sidebar-toggle':
SidebarToggle({ component });
window.influxdatadocs[componentName] = SidebarToggle;
break;
case 'theme':
Theme({ component });
window.influxdatadocs[componentName] = Theme;
break;
// CodeControls();
// ContentInteractions();
// CustomTimestamps();
// Diagram();
// FluxGroupKeysExample();
// FluxInfluxDBVersionsModal();
// InfluxDBUrl();
// Modal();
// PageFeedback();
// ReleaseTOC();
// SearchInput();
// showNotifications();
// Sidebar();
// TabbedContent();
// ThemeSwitch({});
// V3Wayfinding();
// VersionSelector();
case 'theme-switch':
ThemeSwitch({ component });
window.influxdatadocs[componentName] = ThemeSwitch;
break;
default:
console.warn(`Unknown component: ${componentName}`);
}
});
});
/**
* Main initialization function
*/
function init() {
// Initialize global namespace and expose core modules
const globals = initGlobals();
// Initialize non-component UI modules
initModules();
// Initialize components from registry
initComponents(globals);
}
// Initialize everything when the DOM is ready
document.addEventListener('DOMContentLoaded', init);
// Export public API
export { initGlobals, componentRegistry };

View File

@ -0,0 +1,2 @@
import:
- hugo.yml

20
config/testing/config.yml Normal file
View File

@ -0,0 +1,20 @@
baseURL: 'http://localhost:1315/'
server:
port: 1315
# Override settings for testing
buildFuture: true
# Configure what content is built in testing env
params:
environment: testing
buildTestContent: true
# Keep your shared content exclusions
ignoreFiles:
- "content/shared/.*"
# Ignore specific warning logs
ignoreLogs:
- warning-goldmark-raw-html

View File

@ -6,14 +6,14 @@ related:
- /influxdb/v2/write-data/
- /influxdb/v2/write-data/quick-start
- https://influxdata.com, This is an external link
draft: true
test_only: true # Custom parameter to indicate test-only content
---
This is a paragraph. Lorem ipsum dolor ({{< icon "trash" "v2" >}}) sit amet, consectetur adipiscing elit. Nunc rutrum, metus id scelerisque euismod, erat ante suscipit nibh, ac congue enim risus id est. Etiam tristique nisi et tristique auctor. Morbi eu bibendum erat. Sed ullamcorper, dui id lobortis efficitur, mauris odio pharetra neque, vel tempor odio dolor blandit justo.
[Ref link][foo]
[foo]: https://docs.influxadata.com
[foo]: https://docs.influxdata.com
This is **bold** text. This is _italic_ text. This is _**bold and italic**_.

View File

@ -0,0 +1,56 @@
---
title: Revoke a database token
description: >
Use the [`influxctl token revoke` command](/influxdb3/clustered/reference/cli/influxctl/token/revoke/)
to revoke a token from your InfluxDB cluster and disable all
permissions associated with the token.
Provide the ID of the token you want to revoke.
menu:
influxdb3_clustered:
parent: Database tokens
weight: 203
list_code_example: |
```sh
influxctl token revoke <TOKEN_ID>
```
aliases:
- /influxdb3/clustered/admin/tokens/delete/
- /influxdb3/clustered/admin/tokens/database/delete/
---
Use the [`influxctl token revoke` command](/influxdb3/clustered/reference/cli/influxctl/token/revoke/)
to revoke a database token from your {{< product-name omit=" Clustered" >}} cluster and disable
all permissions associated with the token.
1. If you haven't already, [download and install the `influxctl` CLI](/influxdb3/clustered/reference/cli/influxctl/#download-and-install-influxctl).
2. Run the [`influxctl token list` command](/influxdb3/clustered/reference/cli/influxctl/token/list)
to output tokens with their IDs.
Copy the **token ID** of the token you want to delete.
```sh
influxctl token list
```
3. Run the `influxctl token revoke` command and provide the following:
- Token ID to revoke
4. Confirm that you want to revoke the token.
{{% code-placeholders "TOKEN_ID" %}}
```sh
influxctl token revoke TOKEN_ID
```
{{% /code-placeholders %}}
> [!Warning]
> #### Revoking a token is immediate and cannot be undone
>
> Revoking a database token is a destructive action that takes place immediately
> and cannot be undone.
>
> #### Rotate revoked tokens
>
> After revoking a database token, any clients using the revoked token need to
> be updated with a new database token to continue to interact with your
> {{% product-name omit=" Clustered" %}} cluster.

View File

@ -1,15 +0,0 @@
---
title: influxdb3 create plugin
description: >
The `influxdb3 create plugin` command creates a new processing engine plugin.
menu:
influxdb3_core:
parent: influxdb3 create
name: influxdb3 create plugin
weight: 400
source: /shared/influxdb3-cli/create/plugin.md
---
<!--
The content of this file is at content/shared/influxdb3-cli/create/plugin.md
-->

View File

@ -1,15 +0,0 @@
---
title: influxdb3 delete plugin
description: >
The `influxdb3 delete plugin` command deletes a processing engine plugin.
menu:
influxdb3_core:
parent: influxdb3 delete
name: influxdb3 delete plugin
weight: 400
source: /shared/influxdb3-cli/delete/last_cache.md
---
<!--
The content of this file is at content/shared/influxdb3-cli/delete/plugin.md
-->

View File

@ -1,15 +0,0 @@
---
title: influxdb3 create plugin
description: >
The `influxdb3 create plugin` command creates a new processing engine plugin.
menu:
influxdb3_enterprise:
parent: influxdb3 create
name: influxdb3 create plugin
weight: 400
source: /shared/influxdb3-cli/create/plugin.md
---
<!--
The content of this file is at content/shared/influxdb3-cli/create/plugin.md
-->

View File

@ -1,15 +0,0 @@
---
title: influxdb3 delete plugin
description: >
The `influxdb3 delete plugin` command deletes a processing engine plugin.
menu:
influxdb3_enterprise:
parent: influxdb3 delete
name: influxdb3 delete plugin
weight: 400
source: /shared/influxdb3-cli/delete/last_cache.md
---
<!--
The content of this file is at content/shared/influxdb3-cli/delete/plugin.md
-->

View File

@ -1,4 +1,66 @@
Manage tokens to authenticate and authorize access to resources and data in your
{{< product-name >}} instance.
Manage tokens to authenticate and authorize access to resources and data in your {{< product-name >}} instance.
## Provide your token
Before running CLI commands or making HTTP API requests, you must provide a valid token to authenticate.
The mechanism for providing your token depends on the client you use to interact with {{% product-name %}}--for example:
{{< tabs-wrapper >}}
{{% tabs %}}
[influxdb3 CLI](#influxdb3-cli-auth)
[cURL](#curl-auth)
{{% /tabs %}}
{{% tab-content %}}
When using the `influxdb3` CLI, you can use the `--token` option to provide your authorization token.
{{% code-placeholders "YOUR_TOKEN" %}}
```bash
# Include the --token option in your influxdb3 command
influxdb3 query \
--token YOUR_TOKEN \
--database example-db \
"SELECT * FROM 'example-table' WHERE time > now() - INTERVAL '10 minutes'"
```
{{% /code-placeholders %}}
You can also set the `INFLUXDB3_AUTH_TOKEN` environment variable to automatically provide your
authorization token to all `influxdb3` commands.
{{% code-placeholders "YOUR_TOKEN" %}}
```bash
# Export your token as an environment variable
export INFLUXDB3_AUTH_TOKEN=YOUR_TOKEN
# Run an influxdb3 command without the --token option
influxdb3 query \
--database example-db \
"SELECT * FROM 'example-table' WHERE time > now() - INTERVAL '10 minutes'"
```
{{% /code-placeholders %}}
Replace `YOUR_TOKEN` with your authorization token.
{{% /tab-content %}}
{{% tab-content %}}
{{% code-placeholders "AUTH_TOKEN" %}}
```bash
# Add your token to the HTTP Authorization header
curl "http://{{< influxdb/host >}}/api/v3/query_sql" \
--header "Authorization: Bearer AUTH_TOKEN" \
--data-urlencode "db=example-db" \
--data-urlencode "q=SELECT * FROM 'example-table' WHERE time > now() - INTERVAL '10 minutes'"
```
{{% /code-placeholders %}}
Replace `AUTH_TOKEN` with your actual InfluxDB 3 token.
{{% /tab-content %}}
{{< /tabs-wrapper >}}
{{< children hlevel="h2" readmore=true hr=true >}}

View File

@ -8,8 +8,12 @@ data and resources in your InfluxDB 3 instance.
> Token metadata includes the hashed token string.
> InfluxDB 3 does not store the raw token string.
In the following examples, replace {{% code-placeholder-key %}}`AUTH_TOKEN`{{% /code-placeholder-key %}} with your InfluxDB {{% token-link "admin" %}}
{{% show-in "enterprise" %}} or a token with read permission on the `_internal` system database`{{% /show-in %}}.
> [!Important]
> #### Required permissions
>
> Listing admin tokens requires a valid InfluxDB {{% token-link "admin" %}}{{% show-in "enterprise" %}} or a token with read access to the `_internal` system database{{% /show-in %}}.
> For more information about providing a token, see [provide your token](/influxdb3/version/admin/tokens/#provide-your-token).
## List all tokens

View File

@ -18,7 +18,6 @@ influxdb3 create <SUBCOMMAND>
| [file_index](/influxdb3/version/reference/cli/influxdb3/create/file_index/) | Create a new file index for a database or table |
| [last_cache](/influxdb3/version/reference/cli/influxdb3/create/last_cache/) | Create a new last value cache |
| [distinct_cache](/influxdb3/version/reference/cli/influxdb3/create/distinct_cache/) | Create a new distinct value cache |
| [plugin](/influxdb3/version/reference/cli/influxdb3/create/plugin/) | Create a new processing engine plugin |
| [table](/influxdb3/version/reference/cli/influxdb3/create/table/) | Create a new table in a database |
| [token](/influxdb3/version/reference/cli/influxdb3/create/token/) | Create a new authentication token |
| [trigger](/influxdb3/version/reference/cli/influxdb3/create/trigger/) | Create a new trigger for the processing engine |

View File

@ -1,5 +1,6 @@
The `influxdb3 create database` command creates a new database in your {{< product-name >}} instance.
The `influxdb3 create database` command creates a new database.
Provide a database name and, optionally, specify connection settings and authentication credentials using flags or environment variables.
## Usage
@ -11,11 +12,10 @@ influxdb3 create database [OPTIONS] <DATABASE_NAME>
## Arguments
- **DATABASE_NAME**: The name of the database to create.
Valid database names are alphanumeric and start with a letter or number.
Dashes (`-`) and underscores (`_`) are allowed.
- **`DATABASE_NAME`**: The name of the database to create. Valid database names are alphanumeric and start with a letter or number. Dashes (-) and underscores (_) are allowed.
Environment variable: `INFLUXDB3_DATABASE_NAME`
You can also set the database name using the `INFLUXDB3_DATABASE_NAME` environment variable.
## Options
@ -29,7 +29,7 @@ influxdb3 create database [OPTIONS] <DATABASE_NAME>
### Option environment variables
You can use the following environment variables to set command options:
You can use the following environment variables instead of providing CLI options directly:
| Environment Variable | Option |
| :------------------------ | :----------- |
@ -38,11 +38,9 @@ You can use the following environment variables to set command options:
## Examples
- [Create a new database](#create-a-new-database)
- [Create a new database while specifying the token inline](#create-a-new-database-while-specifying-the-token-inline)
In the examples below, replace the following:
The following examples show how to create a database.
In your commands replace the following:
- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}:
Database name
- {{% code-placeholder-key %}}`AUTH_TOKEN`{{% /code-placeholder-key %}}:
@ -50,7 +48,9 @@ In the examples below, replace the following:
{{% code-placeholders "DATABASE_NAME|AUTH_TOKEN" %}}
### Create a new database
### Create a database (default)
Creates a database using settings from environment variables and defaults.
<!--pytest.mark.skip-->
@ -58,7 +58,10 @@ In the examples below, replace the following:
influxdb3 create database DATABASE_NAME
```
### Create a new database while specifying the token inline
### Create a database with an authentication token
Creates a database using the specified arguments.
Flags override their associated environment variables.
<!--pytest.mark.skip-->

View File

@ -1,5 +1,6 @@
The `influxdb3 create distinct_cache` command creates a new distinct value cache for a specific table and column set in your {{< product-name >}} instance.
The `influxdb3 create distinct_cache` command creates a new distinct value cache.
Use this command to configure a cache that tracks unique values in specified columns. You must provide the database, token, table, and columns. Optionally, you can specify a name for the cache.
## Usage
@ -16,10 +17,9 @@ influxdb3 create distinct_cache [OPTIONS] \
## Arguments
- **CACHE_NAME**: _(Optional)_ Name for the cache.
If not provided, the command automatically generates a name.
- **`CACHE_NAME`**: _(Optional)_ A name to assign to the cache. If omitted, the CLI generates a name automatically.
## Options
## Options
| Option | | Description |
| :----- | :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@ -52,4 +52,69 @@ You can use the following environment variables to set command options:
| `INFLUXDB3_DATABASE_NAME` | `--database` |
| `INFLUXDB3_AUTH_TOKEN` | `--token` |
<!-- TODO: GET EXAMPLES -->
## Prerequisites
Before creating a distinct value cache, make sure you:
1. [Create a database](/influxdb3/version/reference/cli/influxdb3/create/database/)
2. [Create a table](/influxdb3/version/reference/cli/influxdb3/create/table/) that includes the columns you want to cache
3. Have a valid authentication token
## Examples
Before running the following commands, replace the placeholder values with your own:
- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}:
The database name
- {{% code-placeholder-key %}}`TABLE_NAME`{{% /code-placeholder-key %}}:
The name of the table to cache values from
- {{% code-placeholder-key %}}`CACHE_NAME`{{% /code-placeholder-key %}}:
The name of the distinct value cache to create
- {{% code-placeholder-key %}}`COLUMN_NAME`{{% /code-placeholder-key %}}: The column to
cache distinct values from
You can also set environment variables (such as `INFLUXDB3_AUTH_TOKEN`) instead of passing options inline.
{{% code-placeholders "(DATABASE|TABLE|COLUMN|CACHE)_NAME" %}}
### Create a distinct cache for one column
Track unique values from a single column. This setup is useful for testing or simple use cases.
<!--pytest.mark.skip-->
```bash
influxdb3 create distinct_cache \
--database DATABASE_NAME \
--table TABLE_NAME \
--column COLUMN_NAME \
CACHE_NAME
```
### Create a hierarchical cache with constraints
Create a distinct value cache for multiple columns. The following example tracks unique combinations of `room` and `sensor_id`, and sets limits on the number of entries and their maximum age.
<!--pytest.mark.skip-->
```bash
influxdb3 create distinct_cache \
--database my_test_db \
--table my_sensor_table \
--columns room,sensor_id \
--max-cardinality 1000 \
--max-age 30d \
my_sensor_distinct_cache
```
{{% /code-placeholders %}}
## Common pitfalls
- `--column` is not valid. Use `--columns`.
- Tokens must be included explicitly unless set via `INFLUXDB3_AUTH_TOKEN`
- Table and column names must already exist or be recognized by the engine

View File

@ -1,18 +1,23 @@
The `influxdb3 create last_cache` command creates a new last value cache.
The `influxdb3 create last_cache` command creates a last value cache, which stores the most recent values for specified columns in a table. Use this to efficiently retrieve the latest values based on key column combinations.
## Usage
{{% code-placeholders "DATABASE_NAME|TABLE_NAME|AUTH_TOKEN|CACHE_NAME" %}}
<!--pytest.mark.skip-->
```bash
influxdb3 create last_cache [OPTIONS] --database <DATABASE_NAME> --table <TABLE> [CACHE_NAME]
influxdb3 create last_cache [OPTIONS] \
--database DATABASE_NAME \
--table TABLE_NAME \
--token AUTH_TOKEN \
CACHE_NAME
```
{{% /code-placeholders %}}
## Arguments
- **CACHE_NAME**: _(Optional)_ Name for the cache.
If not provided, the command automatically generates a name.
- **CACHE_NAME**: _(Optional)_ Name for the cache. If omitted, InfluxDB automatically generates one.
## Options
@ -32,7 +37,7 @@ influxdb3 create last_cache [OPTIONS] --database <DATABASE_NAME> --table <TABLE>
### Option environment variables
You can use the following environment variables to set command options:
You can use the following environment variables as substitutes for CLI options:
| Environment Variable | Option |
| :------------------------ | :----------- |
@ -40,4 +45,59 @@ You can use the following environment variables to set command options:
| `INFLUXDB3_DATABASE_NAME` | `--database` |
| `INFLUXDB3_AUTH_TOKEN` | `--token` |
<!-- TODO: GET EXAMPLES -->
## Prerequisites
Before creating a last value cache, ensure youve done the following:
- Create a [database](/influxdb3/version/reference/cli/influxdb3/create/database/).
- Create a [table](/influxdb3/version/reference/cli/influxdb3/create/table/) with the columns you want to cache.
- Have a valid authentication token.
## Examples
A last value cache stores the most recent values from specified columns in a table.
### Create a basic last value cache for one column
The following example shows how to track the most recent value for a single key (the last temperature for each room):
<!--pytest.mark.skip-->
```bash
influxdb3 create last_cache \
--database DATABASE_NAME \
--table my_sensor_table \
--token AUTH_TOKEN \
--key-columns room \
--value-columns temp \
my_temp_cache
```
### Create a last value cache with multiple keys and values
The following example shows how to:
- Use multiple columns as a composite key
- Track several values per key combination
- Set a cache entry limit with `--count`
- Configure automatic expiry with `--ttl`
<!--pytest.mark.skip-->
```bash
influxdb3 create last_cache \
--database DATABASE_NAME \
--table my_sensor_table \
--token AUTH_TOKEN \
--key-columns room,sensor_id \
--value-columns temp,hum \
--count 10 \
--ttl 1h \
my_sensor_cache
```
## Usage notes
- Define the table schema to include all specified key and value columns.
- Pass tokens using `--token`, unless you've set one through an environment variable.
- Specify `--count` and `--ttl` to override the defaults; otherwise, the system uses default values.

View File

@ -1,45 +0,0 @@
The `influxdb3 create plugin` command creates a new processing engine plugin.
## Usage
<!--pytest.mark.skip-->
```bash
influxdb3 create plugin [OPTIONS] \
--database <DATABASE_NAME> \
--token <AUTH_TOKEN> \
--filename <PLUGIN_FILENAME> \
--entry-point <FUNCTION_NAME> \
<PLUGIN_NAME>
```
## Arguments
- **PLUGIN_NAME**: The name of the plugin to create.
## Options
| Option | | Description |
| :----- | :-------------- | :--------------------------------------------------------------------------------------- |
| `-H` | `--host` | Host URL of the running {{< product-name >}} server (default is `http://127.0.0.1:8181`) |
| `-d` | `--database` | _({{< req >}})_ Name of the database to operate on |
| | `--token` | _({{< req >}})_ Authentication token |
| | `--filename` | _({{< req >}})_ Name of the plugin Python file in the plugin directory |
| | `--entry-point` | _({{< req >}})_ Entry point function name for the plugin |
| | `--plugin-type` | Type of trigger the plugin processes (default is `wal_rows`) |
| | `--tls-ca` | Path to a custom TLS certificate authority (for testing or self-signed certificates) |
| `-h` | `--help` | Print help information |
| | `--help-all` | Print detailed help information |
### Option environment variables
You can use the following environment variables to set command options:
| Environment Variable | Option |
| :------------------------ | :----------- |
| `INFLUXDB3_HOST_URL` | `--host` |
| `INFLUXDB3_DATABASE_NAME` | `--database` |
| `INFLUXDB3_AUTH_TOKEN` | `--token` |
<!-- TODO: GET EXAMPLES -->

View File

@ -1,5 +1,10 @@
The `influxdb3 create table` command creates a table in a database.
The `influxdb3 create table` command creates a new table in a specified database. Tables must include at least one tag column and can optionally include field columns with defined data types.
> [!Note]
> InfluxDB automatically creates tables when you write line protocol data. Use this command
> only if you need to define a custom schema or apply a custom partition template before
> writing data.
## Usage
@ -39,7 +44,7 @@ influxdb3 create table [OPTIONS] \
### Option environment variables
You can use the following environment variables to set command options:
You can use the following environment variables to set options instead of passing them via CLI flags:
| Environment Variable | Option |
| :------------------------ | :----------- |
@ -49,21 +54,20 @@ You can use the following environment variables to set command options:
## Examples
- [Create a table](#create-a-table)
- [Create a table with tag and field columns](#create-a-table-with-tag-and-field-columns)
In the examples below, replace the following:
In the following examples, replace each placeholder with your actual values:
- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}:
Database name
The database name
- {{% code-placeholder-key %}}`AUTH_TOKEN`{{% /code-placeholder-key %}}:
Authentication token
- {{% code-placeholder-key %}}`TABLE_NAME`{{% /code-placeholder-key %}}:
Table name
A name for the new table
{{% code-placeholders "(DATABASE|TABLE)_NAME" %}}
{{% code-placeholders "DATABASE_NAME|TABLE_NAME|AUTH_TOKEN" %}}
### Create a table
### Create an empty table
<!--pytest.mark.skip-->
```bash
influxdb3 create table \
@ -86,4 +90,31 @@ influxdb3 create table \
TABLE_NAME
```
### Verification
Use the `SHOW TABLES` query to verify that the table was created successfully:
<!--pytest.mark.skip-->
```bash
influxdb3 query \
--database my_test_db \
--token AUTH_TOKEN \
"SHOW TABLES"
Example output:
+---------------+--------------------+----------------------------+------------+
| table_catalog | table_schema | table_name | table_type |
+---------------+--------------------+----------------------------+------------+
| public | iox | my_sensor_table | BASE TABLE |
| public | system | distinct_caches | BASE TABLE |
| public | system | last_caches | BASE TABLE |
| public | system | parquet_files | BASE TABLE |
+---------------+--------------------+----------------------------+------------+
```
>[!Note]
> `SHOW TABLES` is an SQL query. It isn't supported in InfluxQL.
{{% /code-placeholders %}}

View File

@ -1,12 +1,14 @@
The `influxdb3 create token` command creates a new authentication token. This returns the raw token string. Use it to authenticate future CLI commands and API requests.
The `influxdb3 create token` command creates a new authentication token.
> [!Important]
> InfluxDB displays the raw token string only once. Be sure to copy and securely store it.
## Usage
<!--pytest.mark.skip-->
```bash
influxdb3 create token <COMMAND> [OPTIONS]
influxdb3 create token <SUBCOMMAND>
```
## Commands
@ -18,15 +20,46 @@ influxdb3 create token <COMMAND> [OPTIONS]
## Options
| Option | | Description |
| :----- | :----------- | :------------------------------ |
| `-h` | `--help` | Print help information |
| | `--help-all` | Print detailed help information |
| Option | | Description |
| :----- | :------- | :--------------------- |
| |`--admin`| Create an admin token |
| `-h` | `--help` | Print help information |
## Examples
### Create an admin token
<!--pytest.mark.skip-->
```bash
influxdb3 create token --admin
```
```
The output is the raw token string you can use to authenticate future CLI commands and API requests.
For CLI commands, use the `--token` option or the `INFLUXDB3_AUTH_TOKEN` environment variable to pass the token string.
### Use the token to create a database
{{% code-placeholders "YOUR_ADMIN_TOKEN|DATABASE_NAME" %}}
<!--pytest.mark.skip-->
```bash
influxdb3 create database \
--token ADMIN_TOKEN \
DATABASE_NAME
```
{{% /code-placeholders %}}
Replace the following:
- {{% code-placeholder-key %}}`ADMIN_TOKEN`{{% /code-placeholder-key %}}: Your InfluxDB admin token
- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}: Name for your new database
> [!Note]
> Set the token as an environment variable to simplify repeated CLI commands:
>
> ```bash
> export INFLUXDB3_AUTH_TOKEN=YOUR_ADMIN_TOKEN
> ```

View File

@ -10,7 +10,6 @@ processing engine.
influxdb3 create trigger [OPTIONS] \
--database <DATABASE_NAME> \
--token <AUTH_TOKEN> \
--plugin <PLUGIN_NAME> \
--trigger-spec <TRIGGER_SPECIFICATION> \
<TRIGGER_NAME>
```
@ -26,7 +25,6 @@ influxdb3 create trigger [OPTIONS] \
| `-H` | `--host` | Host URL of the running {{< product-name >}} server (default is `http://127.0.0.1:8181`) |
| `-d` | `--database` | _({{< req >}})_ Name of the database to operate on |
| | `--token` | _({{< req >}})_ Authentication token |
| | `--plugin` | Plugin to execute when the trigger fires |
| | `--trigger-spec` | Trigger specification--for example `table:<TABLE_NAME>` or `all_tables` |
| | `--disabled` | Create the trigger in disabled state |
| | `--tls-ca` | Path to a custom TLS certificate authority (for testing or self-signed certificates) |
@ -43,4 +41,66 @@ You can use the following environment variables to set command options:
| `INFLUXDB3_DATABASE_NAME` | `--database` |
| `INFLUXDB3_AUTH_TOKEN` | `--token` |
<!-- TODO: GET EXAMPLES -->
## Examples
The following examples show how to use the `influxdb3 create trigger` command to create triggers in different scenarios.
- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}: Database name
- {{% code-placeholder-key %}}`AUTH_TOKEN`{{% /code-placeholder-key %}}:
Authentication token
- {{% code-placeholder-key %}}`TRIGGER_NAME`{{% /code-placeholder-key %}}:
Name of the trigger to create
- {{% code-placeholder-key %}}`TABLE_NAME`{{% /code-placeholder-key %}}:
Name of the table to trigger on
{{% code-placeholders "(DATABASE|TRIGGER)_NAME|AUTH_TOKEN|TABLE_NAME" %}}
### Create a trigger for a specific table
Create a trigger that processes data from a specific table.
<!--pytest.mark.skip-->
```bash
influxdb3 create trigger \
--database DATABASE_NAME \
--token AUTH_TOKEN \
--trigger-spec table:TABLE_NAME \
TRIGGER_NAME
```
### Create a trigger for all tables
Create a trigger that applies to all tables in the specified database.
<!--pytest.mark.skip-->
```bash
influxdb3 create trigger \
--database DATABASE_NAME \
--token AUTH_TOKEN \
--trigger-spec all_tables \
TRIGGER_NAME
```
This is useful when you want a trigger to apply to any table in the database, regardless of name.
### Create a disabled trigger
Create a trigger in a disabled state.
<!--pytest.mark.skip-->
```bash
influxdb3 create trigger \
--disabled \
--database DATABASE_NAME \
--token AUTH_TOKEN \
--trigger-spec table:TABLE_NAME \
TRIGGER_NAME
```
Creating a trigger in a disabled state prevents it from running immediately. You can enable it later when you're ready to activate it.
{{% /code-placeholders %}}

View File

@ -11,7 +11,7 @@ influxdb3 delete database [OPTIONS] <DATABASE_NAME>
## Arguments
- **DATABASE_NAME**: The name of the database to delete.
- **DATABASE_NAME**: The name of the database to delete. Valid database names are alphanumeric and start with a letter or number. Dashes (`-`) and underscores (`_`) are allowed.
Environment variable: `INFLUXDB3_DATABASE_NAME`

View File

@ -1,61 +0,0 @@
The `influxdb3 delete plugin` command deletes a processing engine plugin.
## Usage
<!--pytest.mark.skip-->
```bash
influxdb3 delete plugin [OPTIONS] --database <DATABASE_NAME> <PLUGIN_NAME>
```
## Arguments
- **PLUGIN_NAME**: The name of the plugin to delete.
## Options
| Option | | Description |
| :----- | :----------- | :--------------------------------------------------------------------------------------- |
| `-H` | `--host` | Host URL of the running {{< product-name >}} server (default is `http://127.0.0.1:8181`) |
| `-d` | `--database` | _({{< req >}})_ Name of the database to operate on |
| | `--token` | _({{< req >}})_ Authentication token |
| | `--tls-ca` | Path to a custom TLS certificate authority (for testing or self-signed certificates) |
| `-h` | `--help` | Print help information |
| | `--help-all` | Print detailed help information |
### Option environment variables
You can use the following environment variables to set command options:
| Environment Variable | Option |
| :------------------------ | :----------- |
| `INFLUXDB3_HOST_URL` | `--host` |
| `INFLUXDB3_DATABASE_NAME` | `--database` |
| `INFLUXDB3_AUTH_TOKEN` | `--token` |
## Examples
### Delete a plugin
{{% code-placeholders "(DATABASE|PLUGIN)_NAME|AUTH_TOKEN" %}}
<!--pytest.mark.skip-->
```bash
influxdb3 delete plugin \
--database DATABASE_NAME \
--token AUTH_TOKEN \
PLUGIN_NAME
```
{{% /code-placeholders %}}
In the example above, replace the following:
- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}:
Database name
- {{% code-placeholder-key %}}`AUTH_TOKEN`{{% /code-placeholder-key %}}:
Authentication token
- {{% code-placeholder-key %}}`PLUGIN_NAME`{{% /code-placeholder-key %}}:
Name of the plugin to delete

View File

@ -5,6 +5,28 @@
> All updates to Core are automatically included in Enterprise.
> The Enterprise sections below only list updates exclusive to Enterprise.
## v3.0.3 {date="2025-05-16"}
**Core**: revision 384c457ef5f0d5ca4981b22855e411d8cac2688e
**Enterprise**: revision 34f4d28295132b9efafebf654e9f6decd1a13caf
### Core
#### Fixes
- Prevent operator token, `_admin`, from being deleted.
### Enterprise
#### Fixes
- Fix object store info digest that is output during onboarding.
- Fix issues with false positive catalog error on shutdown.
- Fix licensing validation issues.
- Other fixes and performance improvements.
## v3.0.2 {date="2025-05-01"}
**Core**: revision d80d6cd60049c7b266794a48c97b1b6438ac5da9

View File

@ -252,14 +252,34 @@ To have the `influxdb3` CLI use your admin token automatically, assign it to the
To create an admin token, use the `influxdb3 create token --admin` subcommand--for example:
{{< code-tabs-wrapper >}}
{{% code-tabs %}}
[CLI](#)
[Docker](#)
{{% /code-tabs %}}
{{% code-tab-content %}}
```bash
influxdb3 create token --admin \
--host http://{{< influxdb/host >}}
--host http://INFLUXDB_HOST
```
{{% /code-tab-content %}}
{{% code-tab-content %}}
{{% code-placeholders "CONTAINER_NAME" %}}
```bash
# With Docker -- In a new terminal, run:
# With Docker — in a new terminal:
docker exec -it CONTAINER_NAME influxdb3 create token --admin
```
{{% /code-placeholders %}}
Replace {{% code-placeholder-key %}}`CONTAINER_NAME`{{% /code-placeholder-key %}} with the name of your running Docker container.
{{% /code-tab-content %}}
{{< /code-tabs-wrapper >}}
The command returns a token string that you can use to authenticate CLI commands and API requests.

View File

@ -282,14 +282,34 @@ To have the `influxdb3` CLI use your admin token automatically, assign it to the
To create an admin token, use the `influxdb3 create token --admin` subcommand--for example:
{{< code-tabs-wrapper >}}
{{% code-tabs %}}
[CLI](#)
[Docker](#)
{{% /code-tabs %}}
{{% code-tab-content %}}
```bash
influxdb3 create token --admin \
--host http://{{< influxdb/host >}}
--host http://INFLUXDB_HOST
```
{{% /code-tab-content %}}
{{% code-tab-content %}}
{{% code-placeholders "CONTAINER_NAME" %}}
```bash
# With Docker -- In a new terminal, run:
# With Docker — in a new terminal:
docker exec -it CONTAINER_NAME influxdb3 create token --admin
```
{{% /code-placeholders %}}
Replace {{% code-placeholder-key %}}`CONTAINER_NAME`{{% /code-placeholder-key %}} with the name of your running Docker container.
{{% /code-tab-content %}}
{{< /code-tabs-wrapper >}}
The command returns a token string that you can use to authenticate CLI commands and API requests.
@ -316,6 +336,7 @@ To create a database token, use the `influxdb3 create token` subcommand and pass
The following example shows how to create a database token that expires in 90 days and has read and write permissions for all databases on the server:
{{% code-placeholders "ADMIN_TOKEN" %}}
```bash
influxdb3 create token \
--permission \
@ -327,8 +348,7 @@ influxdb3 create token \
```
{{% /code-placeholders %}}
In your command, replace {{% code-placeholder-key %}} `ADMIN_TOKEN`{{% /code-placeholder-key %}}
with the admin token you created earlier.
In your command, replace {{% code-placeholder-key %}} `ADMIN_TOKEN`{{% /code-placeholder-key %}} with the admin token you created earlier.
#### Create a system token
@ -355,6 +375,8 @@ To create a system token, use the `influxdb3 create token` subcommand and pass t
The following example shows how to create a system token that expires in 1 year and has read permissions for all system endpoints on the server:
{{% code-placeholders "ADMIN_TOKEN" %}}
```bash
influxdb3 create token \
--permission \
@ -364,6 +386,9 @@ influxdb3 create token \
--name "all system endpoints" \
"system:*:read"
```
{{% /code-placeholders %}}
In your command, replace {{% code-placeholder-key %}} `ADMIN_TOKEN`{{% /code-placeholder-key %}} with the admin token you created earlier.
For more information, see how to [Manage resource tokens](/influxdb3/version/admin/tokens/resource/).
@ -372,14 +397,18 @@ For more information, see how to [Manage resource tokens](/influxdb3/version/adm
- To authenticate `influxdb3` CLI commands, use the `--token` option or assign your
token to the `INFLUXDB3_AUTH_TOKEN` environment variable for `influxdb3` to use it automatically.
- To authenticate HTTP API requests, include `Bearer <TOKEN>` in the `Authorization` header value--for example:
{{% code-placeholders "SYSTEM_TOKEN" %}}
```bash
curl "http://{{< influxdb/host >}}/health" \
--header "Authorization: Bearer SYSTEM_TOKEN"
```
{{% /code-placeholders %}}
In your request, replace
{{% code-placeholder-key %}}`SYSTEM_TOKEN`{{% /code-placeholder-key %}} with the system token you created earlier.
Replace the following:
In your command, replace {{% code-placeholder-key %}}`SYSTEM_TOKEN`{{% /code-placeholder-key %}}: System token that grants access to system endpoints (`/health`, `/metrics`, etc.)
### Data model

View File

@ -1,10 +1,17 @@
const { defineConfig } = require('cypress');
const process = require('process');
import { defineConfig } from 'cypress';
import { cwd as _cwd } from 'process';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import {
BROKEN_LINKS_FILE,
FIRST_BROKEN_LINK_FILE,
initializeReport,
readBrokenLinksReport,
} from './cypress/support/link-reporter.js';
module.exports = defineConfig({
export default defineConfig({
e2e: {
// Automatically prefix cy.visit() and cy.request() commands with a baseUrl.
baseUrl: 'http://localhost:1313',
baseUrl: 'http://localhost:1315',
defaultCommandTimeout: 10000,
pageLoadTimeout: 30000,
responseTimeout: 30000,
@ -12,34 +19,177 @@ module.exports = defineConfig({
numTestsKeptInMemory: 5,
projectId: 'influxdata-docs',
setupNodeEvents(on, config) {
// implement node event listeners here
// Browser setup
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
// Force Chrome to use a less memory-intensive approach
launchOptions.args.push('--disable-dev-shm-usage');
launchOptions.args.push('--disable-gpu');
launchOptions.args.push('--disable-extensions');
return launchOptions;
}
});
on('task', {
// Fetch the product list configured in /data/products.yml
getData(filename) {
return new Promise((resolve, reject) => {
const yq = require('js-yaml');
const fs = require('fs');
const cwd = process.cwd();
const cwd = _cwd();
try {
resolve(
yq.load(fs.readFileSync(`${cwd}/data/${filename}.yml`, 'utf8'))
yaml.load(
fs.readFileSync(`${cwd}/data/${filename}.yml`, 'utf8')
)
);
} catch (e) {
reject(e);
}
});
},
// Log task for reporting
log(message) {
if (typeof message === 'object') {
if (message.type === 'error') {
console.error(`\x1b[31m${message.message}\x1b[0m`); // Red
} else if (message.type === 'warning') {
console.warn(`\x1b[33m${message.message}\x1b[0m`); // Yellow
} else if (message.type === 'success') {
console.log(`\x1b[32m${message.message}\x1b[0m`); // Green
} else if (message.type === 'divider') {
console.log(`\x1b[90m${message.message}\x1b[0m`); // Gray
} else {
console.log(message.message || message);
}
} else {
console.log(message);
}
return null;
},
// File tasks
writeFile({ path, content }) {
try {
fs.writeFileSync(path, content);
return true;
} catch (error) {
console.error(`Error writing to file ${path}: ${error.message}`);
return { error: error.message };
}
},
readFile(path) {
try {
return fs.existsSync(path) ? fs.readFileSync(path, 'utf8') : null;
} catch (error) {
console.error(`Error reading file ${path}: ${error.message}`);
return { error: error.message };
}
},
// Broken links reporting tasks
initializeBrokenLinksReport() {
return initializeReport();
},
// Special case domains are now handled directly in the test without additional reporting
// This task is kept for backward compatibility but doesn't do anything special
reportSpecialCaseLink(linkData) {
console.log(
`✅ Expected status code: ${linkData.url} (status: ${linkData.status}) is valid for this domain`
);
return true;
},
reportBrokenLink(linkData) {
try {
// Validate link data
if (!linkData || !linkData.url || !linkData.page) {
console.error('Invalid link data provided');
return false;
}
// Read current report
const report = readBrokenLinksReport();
// Find or create entry for this page
let pageReport = report.find((r) => r.page === linkData.page);
if (!pageReport) {
pageReport = { page: linkData.page, links: [] };
report.push(pageReport);
}
// Check if link is already in the report to avoid duplicates
const isDuplicate = pageReport.links.some(
(link) => link.url === linkData.url && link.type === linkData.type
);
if (!isDuplicate) {
// Add the broken link to the page's report
pageReport.links.push({
url: linkData.url,
status: linkData.status,
type: linkData.type,
linkText: linkData.linkText,
});
// Write updated report back to file
fs.writeFileSync(
BROKEN_LINKS_FILE,
JSON.stringify(report, null, 2)
);
// Store first broken link if not already recorded
const firstBrokenLinkExists =
fs.existsSync(FIRST_BROKEN_LINK_FILE) &&
fs.readFileSync(FIRST_BROKEN_LINK_FILE, 'utf8').trim() !== '';
if (!firstBrokenLinkExists) {
// Store first broken link with complete information
const firstBrokenLink = {
url: linkData.url,
status: linkData.status,
type: linkData.type,
linkText: linkData.linkText,
page: linkData.page,
time: new Date().toISOString(),
};
fs.writeFileSync(
FIRST_BROKEN_LINK_FILE,
JSON.stringify(firstBrokenLink, null, 2)
);
console.error(
`🔴 FIRST BROKEN LINK: ${linkData.url} (${linkData.status}) - ${linkData.type} on page ${linkData.page}`
);
}
// Log the broken link immediately to console
console.error(
`❌ BROKEN LINK: ${linkData.url} (${linkData.status}) - ${linkData.type} on page ${linkData.page}`
);
}
return true;
} catch (error) {
console.error(`Error reporting broken link: ${error.message}`);
// Even if there's an error, we want to ensure the test knows there was a broken link
return true;
}
},
});
// Load plugins file using dynamic import for ESM compatibility
return import('./cypress/plugins/index.js').then((module) => {
return module.default(on, config);
});
return config;
},
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/e2e.js',
viewportWidth: 1280,
viewportHeight: 720,
},
env: {
test_subjects: '',
},
});

View File

@ -1,11 +1,17 @@
/// <reference types="cypress" />
describe('Article links', () => {
describe('Article', () => {
const subjects = Cypress.env('test_subjects').split(',');
// Always use HEAD for downloads to avoid timeouts
const useHeadForDownloads = true;
// Helper function to identify download links - improved
// Set up initialization for tests
before(() => {
// Initialize the broken links report
cy.task('initializeBrokenLinksReport');
});
// Helper function to identify download links
function isDownloadLink(href) {
// Check for common download file extensions
const downloadExtensions = [
@ -45,130 +51,192 @@ describe('Article links', () => {
}
// Helper function to make appropriate request based on link type
function testLink(href) {
function testLink(href, linkText = '', pageUrl) {
// Common request options for both methods
const requestOptions = {
failOnStatusCode: true,
timeout: 15000, // Increased timeout for reliability
followRedirect: true, // Explicitly follow redirects
retryOnNetworkFailure: true, // Retry on network issues
retryOnStatusCodeFailure: true, // Retry on 5xx errors
};
function handleFailedLink(url, status, type, redirectChain = '') {
// Report the broken link
cy.task('reportBrokenLink', {
url: url + redirectChain,
status,
type,
linkText,
page: pageUrl,
});
// Throw error for broken links
throw new Error(
`BROKEN ${type.toUpperCase()} LINK: ${url} (status: ${status})${redirectChain} on ${pageUrl}`
);
}
if (useHeadForDownloads && isDownloadLink(href)) {
cy.log(`** Testing download link with HEAD: ${href} **`);
cy.request({
method: 'HEAD',
url: href,
...requestOptions,
}).then((response) => {
const message = `Link is broken: ${href} (status: ${response.status})`;
try {
expect(response.status).to.be.lt(400);
} catch (e) {
// Log the broken link with the URL for better visibility in reports
cy.log(`❌ BROKEN LINK: ${href} (${response.status})`);
throw new Error(message);
// Check final status after following any redirects
if (response.status >= 400) {
// Build redirect info string if available
const redirectInfo =
response.redirects && response.redirects.length > 0
? ` (redirected to: ${response.redirects.join(' -> ')})`
: '';
handleFailedLink(href, response.status, 'download', redirectInfo);
}
});
} else {
cy.log(`** Testing link: ${href} **`);
cy.log(JSON.stringify(requestOptions));
cy.request({
url: href,
failOnStatusCode: false,
timeout: 10000, // 10 second timeout for regular links
...requestOptions,
}).then((response) => {
const message = `Link is broken: ${href} (status: ${response.status})`;
try {
expect(response.status).to.be.lt(400);
} catch (e) {
// Log the broken link with the URL for better visibility in reports
cy.log(`❌ BROKEN LINK: ${href} (${response.status})`);
throw new Error(message);
// Check final status after following any redirects
if (response.status >= 400) {
// Build redirect info string if available
const redirectInfo =
response.redirects && response.redirects.length > 0
? ` (redirected to: ${response.redirects.join(' -> ')})`
: '';
handleFailedLink(href, response.status, 'regular', redirectInfo);
}
});
}
}
// Test implementation for subjects
subjects.forEach((subject) => {
it(`contains valid internal links on ${subject}`, function () {
cy.visit(`${subject}`);
it(`${subject} has valid internal links`, function () {
cy.visit(`${subject}`, { timeout: 20000 });
// Test internal links
// 1. Timeout and fail the test if article is not found
// 2. Check each link.
// 3. If no links are found, continue without failing
cy.get('article').then(($article) => {
cy.get('article, .api-content').then(($article) => {
// Find links without failing the test if none are found
const $links = $article.find('a[href^="/"]');
if ($links.length === 0) {
cy.log('No internal links found on this page');
return;
}
// Now test each link
cy.wrap($links).each(($a) => {
const href = $a.attr('href');
testLink(href);
const linkText = $a.text().trim();
testLink(href, linkText, subject);
});
});
});
it(`checks anchor links on ${subject} (with warnings for missing targets)`, function () {
it(`${subject} has valid anchor links`, function () {
cy.visit(`${subject}`);
// Track missing anchors for summary
const missingAnchors = [];
// Define selectors for anchor links to ignore, such as behavior triggers
const ignoreLinks = ['.tabs a[href^="#"]', '.code-tabs a[href^="#"]'];
// Process anchor links individually
cy.get('article').then(($article) => {
const $anchorLinks = $article.find('a[href^="#"]');
const anchorSelector =
'a[href^="#"]:not(' + ignoreLinks.join('):not(') + ')';
cy.get('article, .api-content').then(($article) => {
const $anchorLinks = $article.find(anchorSelector);
if ($anchorLinks.length === 0) {
cy.log('No anchor links found on this page');
return;
}
cy.wrap($anchorLinks).each(($a) => {
const href = $a.prop('href');
if (href && href.length > 1) {
// Skip empty anchors (#)
// Get just the fragment part
const url = new URL(href);
const anchorId = url.hash.substring(1); // Remove the # character
const href = $a.prop('href');
const linkText = $a.text().trim();
if (!anchorId) {
cy.log(`Skipping empty anchor in ${href}`);
return;
if (href && href.length > 1) {
// Get just the fragment part
const url = new URL(href);
const anchorId = url.hash.substring(1); // Remove the # character
if (!anchorId) {
cy.log(`Skipping empty anchor in ${href}`);
return;
}
// Use DOM to check if the element exists
cy.window().then((win) => {
const element = win.document.getElementById(anchorId);
if (!element) {
cy.task('reportBrokenLink', {
url: `#${anchorId}`,
status: 404,
type: 'anchor',
linkText,
page: subject,
});
cy.log(`⚠️ Missing anchor target: #${anchorId}`);
}
// Use DOM to check if the element exists, but don't fail if missing
cy.window().then((win) => {
const element = win.document.getElementById(anchorId);
if (element) {
cy.log(`✅ Anchor target exists: #${anchorId}`);
} else {
// Just warn about the missing anchor
cy.log(`⚠️ WARNING: Missing anchor target: #${anchorId}`);
missingAnchors.push(anchorId);
}
});
}
})
.then(() => {
// After checking all anchors, log a summary
if (missingAnchors.length > 0) {
cy.log(
`⚠️ Found ${missingAnchors.length} missing anchor targets: ${missingAnchors.join(', ')}`
);
} else {
cy.log('✅ All anchor targets are valid');
}
});
});
it(`contains valid external links on ${subject}`, function () {
cy.visit(`${subject}`);
// Test external links
// 1. Timeout and fail the test if article is not found
// 2. Check each link.
// 3. If no links are found, continue without failing
cy.get('article').then(($article) => {
// Find links without failing the test if none are found
const $links = $article.find('a[href^="http"]');
if ($links.length === 0) {
cy.log('No external links found on this page');
return;
});
}
cy.wrap($links).each(($a) => {
const href = $a.attr('href');
testLink(href);
});
});
});
});
it(`${subject} has valid external links`, function () {
// Check if we should skip external links entirely
if (Cypress.env('skipExternalLinks') === true) {
cy.log(
'Skipping all external links as configured by skipExternalLinks'
);
return;
}
cy.visit(`${subject}`);
// Define allowed external domains to test
const allowedExternalDomains = ['github.com', 'kapa.ai'];
// Test external links
cy.get('article, .api-content').then(($article) => {
// Find links without failing the test if none are found
const $links = $article.find('a[href^="http"]');
if ($links.length === 0) {
cy.log('No external links found on this page');
return;
}
// Filter links to only include allowed domains
const $allowedLinks = $links.filter((_, el) => {
const href = el.getAttribute('href');
try {
const url = new URL(href);
return allowedExternalDomains.some(
(domain) =>
url.hostname === domain || url.hostname.endsWith(`.${domain}`)
);
} catch (e) {
return false;
}
});
if ($allowedLinks.length === 0) {
cy.log('No links to allowed external domains found on this page');
return;
}
cy.log(
`Found ${$allowedLinks.length} links to allowed external domains to test`
);
cy.wrap($allowedLinks).each(($a) => {
const href = $a.attr('href');
const linkText = $a.text().trim();
testLink(href, linkText, subject);
});
});
});

View File

View File

@ -0,0 +1,107 @@
/// <reference types="cypress" />
describe('Stable version', function () {
before(function () {
// Track JavaScript errors
cy.on('uncaught:exception', (err, runnable) => {
// Log the error to the Cypress command log
cy.log(`JavaScript error: ${err.message}`);
// Add the error to the test failure message
Cypress.failures = Cypress.failures || [];
Cypress.failures.push(err.message);
// Return false to prevent Cypress from failing the test
return false;
});
});
beforeEach(function () {
// Clear any stored failures before each test
Cypress.failures = [];
});
it('should show InfluxDB 3 Core as successor product in InfluxDB v2 page', function () {
// Visit the v2 documentation page
cy.visit('/influxdb/v1/introduction/install/');
// Check for the warning block that appears for older versions
cy.get('.warn.block.old-version').should('exist');
// Verify that the warning message references original product name
cy.get('.warn.block.old-version p').should(
'contain',
'This page documents an earlier version of InfluxDB OSS'
);
// Check for the link to the successor product
cy.get('.warn.block.old-version a')
.first()
.should('contain', 'InfluxDB 3 Core')
.and('have.attr', 'href', '/influxdb3/core/');
// Verify no JavaScript errors were recorded
cy.wrap(Cypress.failures).should(
'be.empty',
'The following JavaScript errors were detected:\n' +
(Cypress.failures || []).join('\n')
);
});
it('should show InfluxDB 3 Core as successor product in InfluxDB v1 page', function () {
// Visit the v1 documentation page
cy.visit('/influxdb/v1/');
// Check for the warning block that appears for older versions
cy.get('.warn.block.old-version').should('exist');
// Verify that the warning message references original product name
cy.get('.warn.block.old-version p').should(
'contain',
'This page documents an earlier version of InfluxDB OSS'
);
// Check for the link to the latest stable version (successor product)
cy.get('.warn.block.old-version a')
.first()
.should('contain', 'InfluxDB 3 Core')
.and('have.attr', 'href', '/influxdb3/core/');
// Verify no JavaScript errors were recorded
cy.wrap(Cypress.failures).should(
'be.empty',
'The following JavaScript errors were detected:\n' +
(Cypress.failures || []).join('\n')
);
});
it('should verify the product succeeded_by relationship is configured correctly', function () {
// Get the product data to verify succeeded_by field
cy.task('getData', 'products').then((productData) => {
// Check succeeded_by relationship in products.yml
expect(productData.influxdb).to.have.property(
'succeeded_by',
'influxdb3_core'
);
// Verify successor product exists
expect(productData).to.have.property('influxdb3_core');
expect(productData.influxdb3_core).to.have.property(
'name',
'InfluxDB 3 Core'
);
});
});
it('should verify behavior if the stable-version.html template changes', function () {
// Visit a page that shouldn't have a successor redirect
cy.visit('/telegraf/v1/');
cy.get('.warn.block.old-version').should('not.exist');
cy.wrap(Cypress.failures).should(
'be.empty',
'The following JavaScript errors were detected:\n' +
(Cypress.failures || []).join('\n')
);
});
});

26
cypress/plugins/index.js Normal file
View File

@ -0,0 +1,26 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
export default (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// NOTE: The log task is now defined in cypress.config.js
// We don't need to register it here to avoid duplication
return config;
};

View File

@ -14,4 +14,4 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import './commands';

View File

@ -0,0 +1,174 @@
import { spawn } from 'child_process';
import fs from 'fs';
import http from 'http';
import net from 'net';
// Hugo server constants
export const HUGO_PORT = 1315;
export const HUGO_LOG_FILE = '/tmp/hugo_server.log';
/**
* Check if a port is already in use
* @param {number} port - The port to check
* @returns {Promise<boolean>} True if port is in use, false otherwise
*/
export async function isPortInUse(port) {
return new Promise((resolve) => {
const tester = net
.createServer()
.once('error', () => resolve(true))
.once('listening', () => {
tester.close();
resolve(false);
})
.listen(port, '127.0.0.1');
});
}
/**
* Start the Hugo server with the specified options
* @param {Object} options - Configuration options for Hugo
* @param {string} options.configFile - Path to Hugo config file (e.g., 'config/testing/config.yml')
* @param {number} options.port - Port number for Hugo server
* @param {boolean} options.buildDrafts - Whether to build draft content
* @param {boolean} options.noHTTPCache - Whether to disable HTTP caching
* @param {string} options.logFile - Path to write Hugo logs
* @returns {Promise<Object>} Child process object
*/
export async function startHugoServer({
configFile = 'config/testing/config.yml',
port = HUGO_PORT,
buildDrafts = true,
noHTTPCache = true,
logFile = HUGO_LOG_FILE,
} = {}) {
console.log(`Starting Hugo server on port ${port}...`);
// Prepare command arguments
const hugoArgs = [
'hugo',
'server',
'--config',
configFile,
'--port',
String(port),
];
if (buildDrafts) {
hugoArgs.push('--buildDrafts');
}
if (noHTTPCache) {
hugoArgs.push('--noHTTPCache');
}
return new Promise((resolve, reject) => {
try {
// Use npx to find and execute Hugo, which will work regardless of installation method
console.log(`Running Hugo with npx: npx ${hugoArgs.join(' ')}`);
const hugoProc = spawn('npx', hugoArgs, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: true,
});
// Check if the process started successfully
if (!hugoProc || !hugoProc.pid) {
return reject(new Error('Failed to start Hugo server via npx'));
}
// Set up logging
if (logFile) {
hugoProc.stdout.on('data', (data) => {
const output = data.toString();
fs.appendFileSync(logFile, output);
process.stdout.write(`Hugo: ${output}`);
});
hugoProc.stderr.on('data', (data) => {
const output = data.toString();
fs.appendFileSync(logFile, output);
process.stderr.write(`Hugo ERROR: ${output}`);
});
}
// Handle process errors
hugoProc.on('error', (err) => {
console.error(`Error in Hugo server process: ${err}`);
reject(err);
});
// Check for early exit
hugoProc.on('close', (code) => {
if (code !== null && code !== 0) {
reject(new Error(`Hugo process exited early with code ${code}`));
}
});
// Resolve with the process object after a short delay to ensure it's running
setTimeout(() => {
if (hugoProc.killed) {
reject(new Error('Hugo process was killed during startup'));
} else {
resolve(hugoProc);
}
}, 500);
} catch (err) {
console.error(`Error starting Hugo server: ${err.message}`);
reject(err);
}
});
}
/**
* Wait for the Hugo server to be ready
* @param {number} timeoutMs - Timeout in milliseconds
* @returns {Promise<void>}
*/
export async function waitForHugoReady(timeoutMs = 30000) {
console.log(
`Waiting for Hugo server to be ready on http://localhost:${HUGO_PORT}...`
);
const startTime = Date.now();
return new Promise((resolve, reject) => {
// Poll the server
function checkServer() {
const req = http.get(`http://localhost:${HUGO_PORT}`, (res) => {
if (res.statusCode === 200) {
resolve();
} else {
// If we get a response but not 200, try again after delay
const elapsed = Date.now() - startTime;
if (elapsed > timeoutMs) {
reject(
new Error(
`Hugo server responded with status ${res.statusCode} after timeout`
)
);
} else {
setTimeout(checkServer, 1000);
}
}
});
req.on('error', (err) => {
// Connection errors are expected while server is starting
const elapsed = Date.now() - startTime;
if (elapsed > timeoutMs) {
reject(
new Error(`Timed out waiting for Hugo server: ${err.message}`)
);
} else {
// Try again after a delay
setTimeout(checkServer, 1000);
}
});
req.end();
}
// Start polling
checkServer();
});
}

View File

@ -0,0 +1,215 @@
/**
* Broken Links Reporter
* Handles collecting, storing, and reporting broken links found during tests
*/
import fs from 'fs';
export const BROKEN_LINKS_FILE = '/tmp/broken_links_report.json';
export const FIRST_BROKEN_LINK_FILE = '/tmp/first_broken_link.json';
const SOURCES_FILE = '/tmp/test_subjects_sources.json';
/**
* Reads the broken links report from the file system
* @returns {Array} Parsed report data or empty array if file doesn't exist
*/
export function readBrokenLinksReport() {
if (!fs.existsSync(BROKEN_LINKS_FILE)) {
return [];
}
try {
const fileContent = fs.readFileSync(BROKEN_LINKS_FILE, 'utf8');
// Check if the file is empty or contains only an empty array
if (!fileContent || fileContent.trim() === '' || fileContent === '[]') {
return [];
}
// Try to parse the JSON content
try {
const parsedContent = JSON.parse(fileContent);
// Ensure the parsed content is an array
if (!Array.isArray(parsedContent)) {
console.error('Broken links report is not an array');
return [];
}
return parsedContent;
} catch (parseErr) {
console.error(
`Error parsing broken links report JSON: ${parseErr.message}`
);
return [];
}
} catch (err) {
console.error(`Error reading broken links report: ${err.message}`);
return [];
}
}
/**
* Reads the sources mapping file
* @returns {Object} A mapping from URLs to their source files
*/
function readSourcesMapping() {
try {
if (fs.existsSync(SOURCES_FILE)) {
const sourcesData = JSON.parse(fs.readFileSync(SOURCES_FILE, 'utf8'));
return sourcesData.reduce((acc, item) => {
if (item.url && item.source) {
acc[item.url] = item.source;
}
return acc;
}, {});
}
} catch (err) {
console.warn(`Warning: Could not read sources mapping: ${err.message}`);
}
return {};
}
/**
* Formats and displays the broken links report to the console
* @param {Array} brokenLinksReport - The report data to display
* @returns {number} The total number of broken links found
*/
export function displayBrokenLinksReport(brokenLinksReport = null) {
// If no report provided, read from file
if (!brokenLinksReport) {
brokenLinksReport = readBrokenLinksReport();
}
// Check both the report and first broken link file to determine if we have broken links
const firstBrokenLink = readFirstBrokenLink();
// Only report "no broken links" if both checks pass
if (
(!brokenLinksReport || brokenLinksReport.length === 0) &&
!firstBrokenLink
) {
console.log('✅ No broken links detected in the validation report');
return 0;
}
// Special case: check if the single broken link file could be missing from the report
if (
firstBrokenLink &&
(!brokenLinksReport || brokenLinksReport.length === 0)
) {
console.error(
'\n⚠ Warning: First broken link record exists but no links in the report.'
);
console.error('This could indicate a reporting issue.');
}
// Load sources mapping
const sourcesMapping = readSourcesMapping();
// Print a prominent header
console.error('\n\n' + '='.repeat(80));
console.error(' 🚨 BROKEN LINKS DETECTED 🚨 ');
console.error('='.repeat(80));
// Show first failing link if available
if (firstBrokenLink) {
console.error('\n🔴 FIRST FAILING LINK:');
console.error(` URL: ${firstBrokenLink.url}`);
console.error(` Status: ${firstBrokenLink.status}`);
console.error(` Type: ${firstBrokenLink.type}`);
console.error(` Page: ${firstBrokenLink.page}`);
if (firstBrokenLink.linkText) {
console.error(
` Link text: "${firstBrokenLink.linkText.substring(0, 50)}${firstBrokenLink.linkText.length > 50 ? '...' : ''}"`
);
}
console.error('-'.repeat(40));
}
let totalBrokenLinks = 0;
brokenLinksReport.forEach((report) => {
console.error(`\n📄 PAGE: ${report.page}`);
// Add source information if available
const source = sourcesMapping[report.page];
if (source) {
console.error(` PAGE CONTENT SOURCE: ${source}`);
}
console.error('-'.repeat(40));
report.links.forEach((link) => {
console.error(`${link.url}`);
console.error(` - Status: ${link.status}`);
console.error(` - Type: ${link.type}`);
if (link.linkText) {
console.error(
` - Link text: "${link.linkText.substring(0, 50)}${link.linkText.length > 50 ? '...' : ''}"`
);
}
console.error('');
totalBrokenLinks++;
});
});
// Print a prominent summary footer
console.error('='.repeat(80));
console.error(`📊 TOTAL BROKEN LINKS FOUND: ${totalBrokenLinks}`);
console.error('='.repeat(80) + '\n');
return totalBrokenLinks;
}
/**
* Reads the first broken link info from the file system
* @returns {Object|null} First broken link data or null if not found
*/
export function readFirstBrokenLink() {
if (!fs.existsSync(FIRST_BROKEN_LINK_FILE)) {
return null;
}
try {
const fileContent = fs.readFileSync(FIRST_BROKEN_LINK_FILE, 'utf8');
// Check if the file is empty or contains whitespace only
if (!fileContent || fileContent.trim() === '') {
return null;
}
// Try to parse the JSON content
try {
return JSON.parse(fileContent);
} catch (parseErr) {
console.error(
`Error parsing first broken link JSON: ${parseErr.message}`
);
return null;
}
} catch (err) {
console.error(`Error reading first broken link: ${err.message}`);
return null;
}
}
/**
* Initialize the broken links report files
* @returns {boolean} True if initialization was successful
*/
export function initializeReport() {
try {
// Create an empty array for the broken links report
fs.writeFileSync(BROKEN_LINKS_FILE, '[]', 'utf8');
// Reset the first broken link file by creating an empty file
// Using empty string as a clear indicator that no broken link has been recorded yet
fs.writeFileSync(FIRST_BROKEN_LINK_FILE, '', 'utf8');
console.debug('🔄 Initialized broken links reporting system');
return true;
} catch (err) {
console.error(`Error initializing broken links report: ${err.message}`);
return false;
}
}

View File

@ -1,79 +1,139 @@
#!/usr/bin/env node
import { execSync } from 'child_process';
import process from 'process';
import fs from 'fs';
import { execSync } from 'child_process';
import matter from 'gray-matter';
// Get file paths from command line arguments
const filePaths = process.argv.slice(2);
const filePaths = process.argv.slice(2).filter((arg) => !arg.startsWith('--'));
// Parse options
const debugMode = process.argv.includes('--debug');
const debugMode = process.argv.includes('--debug'); // deprecated, no longer used
const jsonMode = process.argv.includes('--json');
// Filter for content files
const contentFiles = filePaths.filter(file =>
file.startsWith('content/') && (file.endsWith('.md') || file.endsWith('.html'))
// Separate shared content files and regular content files
const sharedContentFiles = filePaths.filter(
(file) =>
file.startsWith('content/shared/') &&
(file.endsWith('.md') || file.endsWith('.html'))
);
if (contentFiles.length === 0) {
console.log('No content files to check.');
const regularContentFiles = filePaths.filter(
(file) =>
file.startsWith('content/') &&
!file.startsWith('content/shared/') &&
(file.endsWith('.md') || file.endsWith('.html'))
);
// Find pages that reference shared content files in their frontmatter
function findPagesReferencingSharedContent(sharedFilePath) {
try {
// Remove the leading "content/" to match how it would appear in frontmatter
const relativePath = sharedFilePath.replace(/^content\//, '');
// Use grep to find files that reference this shared content in frontmatter
// Look for source: <path> pattern in YAML frontmatter
const grepCmd = `grep -l "source: .*${relativePath}" --include="*.md" --include="*.html" -r content/`;
// Execute grep command and parse results
const result = execSync(grepCmd, { encoding: 'utf8' }).trim();
if (!result) {
return [];
}
return result.split('\n').filter(Boolean);
} catch (error) {
// grep returns non-zero exit code when no matches are found
if (error.status === 1) {
return [];
}
console.error(
`Error finding references to ${sharedFilePath}: ${error.message}`
);
return [];
}
}
/**
* Extract source from frontmatter or use the file path as source
* @param {string} filePath - Path to the file
* @returns {string} Source path
*/
function extractSourceFromFile(filePath) {
try {
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data } = matter(fileContent);
// If source is specified in frontmatter, return it
if (data.source) {
if (data.source.startsWith('/shared')) {
return 'content' + data.source;
}
return data.source;
}
}
// If no source in frontmatter or can't read file, use the file path itself
return filePath;
} catch (error) {
console.error(`Error extracting source from ${filePath}: ${error.message}`);
return filePath;
}
}
// Process shared content files to find pages that reference them
let pagesToTest = [...regularContentFiles];
if (sharedContentFiles.length > 0) {
console.log(
`Processing ${sharedContentFiles.length} shared content files...`
);
for (const sharedFile of sharedContentFiles) {
const referencingPages = findPagesReferencingSharedContent(sharedFile);
if (referencingPages.length > 0) {
console.log(
`Found ${referencingPages.length} pages referencing ${sharedFile}`
);
// Add referencing pages to the list of pages to test (avoid duplicates)
pagesToTest = [...new Set([...pagesToTest, ...referencingPages])];
} else {
console.log(`No pages found referencing ${sharedFile}`);
}
}
}
if (pagesToTest.length === 0) {
console.log('No content files to map.');
process.exit(0);
}
// Map file paths to URL paths
function mapFilePathToUrl(filePath) {
// Remove content/ prefix
// Map file paths to URL paths and source information
function mapFilePathToUrlAndSource(filePath) {
// Map to URL
let url = filePath.replace(/^content/, '');
// Handle _index files (both .html and .md)
url = url.replace(/\/_index\.(html|md)$/, '/');
// Handle regular .md files
url = url.replace(/\.md$/, '/');
// Handle regular .html files
url = url.replace(/\.html$/, '/');
// Ensure URL starts with a slash
if (!url.startsWith('/')) {
url = '/' + url;
}
return url;
// Extract source
const source = extractSourceFromFile(filePath);
return { url, source };
}
const urls = contentFiles.map(mapFilePathToUrl);
const urlList = urls.join(',');
const mappedFiles = pagesToTest.map(mapFilePathToUrlAndSource);
console.log(`Testing links in URLs: ${urlList}`);
// Create environment object with the cypress_test_subjects variable
const envVars = {
...process.env,
cypress_test_subjects: urlList,
NODE_OPTIONS: '--max-http-header-size=80000 --max-old-space-size=4096'
};
// Run Cypress tests with the mapped URLs
try {
// Choose run mode based on debug flag
if (debugMode) {
// For debug mode, set the environment variable and open Cypress
// The user will need to manually select the test file
console.log('Opening Cypress in debug mode.');
console.log('Please select the "article-links.cy.js" test file when Cypress opens.');
execSync('npx cypress open --e2e', {
stdio: 'inherit',
env: envVars
});
} else {
// For normal mode, run the test automatically
execSync(`npx cypress run --spec "cypress/e2e/content/article-links.cy.js"`, {
stdio: 'inherit',
env: envVars
});
}
} catch (error) {
console.error('Link check failed:', error);
process.exit(1);
}
if (jsonMode) {
console.log(JSON.stringify(mappedFiles, null, 2));
} else {
// Print URL and source info in a format that's easy to parse
mappedFiles.forEach((item) => console.log(`${item.url}|${item.source}`));
}

View File

@ -1,79 +0,0 @@
#!/usr/bin/env node
import { execSync } from 'child_process';
import process from 'process';
// Get file paths from command line arguments
const filePaths = process.argv.slice(2);
// Parse options
const debugMode = process.argv.includes('--debug');
// Filter for content files
const contentFiles = filePaths.filter(file =>
file.startsWith('content/') && (file.endsWith('.md') || file.endsWith('.html'))
);
if (contentFiles.length === 0) {
console.log('No content files to check.');
process.exit(0);
}
// Map file paths to URL paths
function mapFilePathToUrl(filePath) {
// Remove content/ prefix
let url = filePath.replace(/^content/, '');
// Handle _index files (both .html and .md)
url = url.replace(/\/_index\.(html|md)$/, '/');
// Handle regular .md files
url = url.replace(/\.md$/, '/');
// Handle regular .html files
url = url.replace(/\.html$/, '/');
// Ensure URL starts with a slash
if (!url.startsWith('/')) {
url = '/' + url;
}
return url;
}
const urls = contentFiles.map(mapFilePathToUrl);
const urlList = urls.join(',');
console.log(`Testing links in URLs: ${urlList}`);
// Create environment object with the cypress_test_subjects variable
const envVars = {
...process.env,
cypress_test_subjects: urlList,
NODE_OPTIONS: '--max-http-header-size=80000 --max-old-space-size=4096'
};
// Run Cypress tests with the mapped URLs
try {
// Choose run mode based on debug flag
if (debugMode) {
// For debug mode, set the environment variable and open Cypress
// The user will need to manually select the test file
console.log('Opening Cypress in debug mode.');
console.log('Please select the "article-links.cy.js" test file when Cypress opens.');
execSync('npx cypress open --e2e', {
stdio: 'inherit',
env: envVars
});
} else {
// For normal mode, run the test automatically
execSync(`npx cypress run --spec "cypress/e2e/content/article-links.cy.js"`, {
stdio: 'inherit',
env: envVars
});
}
} catch (error) {
console.error('Link check failed');
process.exit(1);
}

View File

@ -0,0 +1,481 @@
/**
* InfluxData Documentation E2E Test Runner
*
* This script automates running Cypress end-to-end tests for the InfluxData documentation site.
* It handles starting a local Hugo server, mapping content files to their URLs, running Cypress tests,
* and reporting broken links.
*
* Usage: node run-e2e-specs.js [file paths...] [--spec test // Display broken links report
const brokenLinksCount = displayBrokenLinksReport();
// Check if we might have special case failures
const hasSpecialCaseFailures =
results &&
results.totalFailed > 0 &&
brokenLinksCount === 0;
if (hasSpecialCaseFailures) {
console.warn(
` Note: Tests failed (${results.totalFailed}) but no broken links were reported. This may be due to special case URLs (like Reddit) that return expected status codes.`
);
}
if (
(results && results.totalFailed && results.totalFailed > 0 && !hasSpecialCaseFailures) ||
brokenLinksCount > 0
) {
console.error(
`⚠️ Tests failed: ${results.totalFailed || 0} test(s) failed, ${brokenLinksCount || 0} broken links found`
);
cypressFailed = true;
exitCode = 1; *
* Example: node run-e2e-specs.js content/influxdb/v2/write-data.md --spec cypress/e2e/content/article-links.cy.js
*/
import { spawn } from 'child_process';
import process from 'process';
import fs from 'fs';
import path from 'path';
import cypress from 'cypress';
import net from 'net';
import matter from 'gray-matter';
import { displayBrokenLinksReport, initializeReport } from './link-reporter.js';
import {
HUGO_PORT,
HUGO_LOG_FILE,
startHugoServer,
waitForHugoReady,
} from './hugo-server.js';
const MAP_SCRIPT = path.resolve('cypress/support/map-files-to-urls.js');
const URLS_FILE = '/tmp/test_subjects.txt';
/**
* Parses command line arguments into file and spec arguments
* @param {string[]} argv - Command line arguments (process.argv)
* @returns {Object} Object containing fileArgs and specArgs arrays
*/
function parseArgs(argv) {
const fileArgs = [];
const specArgs = [];
let i = 2; // Start at index 2 to skip 'node' and script name
while (i < argv.length) {
if (argv[i] === '--spec') {
i++;
if (i < argv.length) {
specArgs.push(argv[i]);
i++;
}
} else {
fileArgs.push(argv[i]);
i++;
}
}
return { fileArgs, specArgs };
}
// Check if port is already in use
async function isPortInUse(port) {
return new Promise((resolve) => {
const tester = net
.createServer()
.once('error', () => resolve(true))
.once('listening', () => {
tester.close();
resolve(false);
})
.listen(port, '127.0.0.1');
});
}
/**
* Extract source information from frontmatter
* @param {string} filePath - Path to the markdown file
* @returns {string|null} Source information if present
*/
function getSourceFromFrontmatter(filePath) {
if (!fs.existsSync(filePath)) {
return null;
}
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data } = matter(fileContent);
return data.source || null;
} catch (err) {
console.warn(
`Warning: Could not extract frontmatter from ${filePath}: ${err.message}`
);
return null;
}
}
/**
* Ensures a directory exists, creating it if necessary
* Also creates an empty file to ensure the directory is not empty
* @param {string} dirPath - The directory path to ensure exists
*/
function ensureDirectoryExists(dirPath) {
if (!fs.existsSync(dirPath)) {
try {
fs.mkdirSync(dirPath, { recursive: true });
console.log(`Created directory: ${dirPath}`);
// Create an empty .gitkeep file to ensure the directory exists and isn't empty
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
} catch (err) {
console.warn(
`Warning: Could not create directory ${dirPath}: ${err.message}`
);
}
}
}
async function main() {
// Keep track of processes to cleanly shut down
let hugoProc = null;
let exitCode = 0;
let hugoStarted = false;
// Add this signal handler to ensure cleanup on unexpected termination
const cleanupAndExit = (code = 1) => {
console.log(`Performing cleanup before exit with code ${code}...`);
if (hugoProc && hugoStarted) {
try {
hugoProc.kill('SIGKILL'); // Use SIGKILL to ensure immediate termination
} catch (err) {
console.error(`Error killing Hugo process: ${err.message}`);
}
}
process.exit(code);
};
// Handle various termination signals
process.on('SIGINT', () => cleanupAndExit(1));
process.on('SIGTERM', () => cleanupAndExit(1));
process.on('uncaughtException', (err) => {
console.error(`Uncaught exception: ${err.message}`);
cleanupAndExit(1);
});
const { fileArgs, specArgs } = parseArgs(process.argv);
if (fileArgs.length === 0) {
console.error('No file paths provided.');
process.exit(1);
}
// Separate content files from non-content files
const contentFiles = fileArgs.filter((file) => file.startsWith('content/'));
const nonContentFiles = fileArgs.filter(
(file) => !file.startsWith('content/')
);
// Log what we're processing
if (contentFiles.length > 0) {
console.log(
`Processing ${contentFiles.length} content files for URL mapping...`
);
}
if (nonContentFiles.length > 0) {
console.log(
`Found ${nonContentFiles.length} non-content files that will be passed directly to tests...`
);
}
let urlList = [];
// Only run the mapper if we have content files
if (contentFiles.length > 0) {
// 1. Map file paths to URLs and write to file
const mapProc = spawn('node', [MAP_SCRIPT, ...contentFiles], {
stdio: ['ignore', 'pipe', 'inherit'],
});
const mappingOutput = [];
mapProc.stdout.on('data', (chunk) => {
mappingOutput.push(chunk.toString());
});
await new Promise((res) => mapProc.on('close', res));
// Process the mapping output
urlList = mappingOutput
.join('')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
// Parse the URL|SOURCE format
if (line.includes('|')) {
const [url, source] = line.split('|');
return { url, source };
} else if (line.startsWith('/')) {
// Handle URLs without source (should not happen with our new code)
return { url: line, source: null };
} else {
// Skip log messages
return null;
}
})
.filter(Boolean); // Remove null entries
}
// Add non-content files directly to be tested, using their path as both URL and source
nonContentFiles.forEach((file) => {
urlList.push({ url: file, source: file });
});
// Log the URLs and sources we'll be testing
console.log(`Found ${urlList.length} items to test:`);
urlList.forEach(({ url, source }) => {
console.log(` URL/FILE: ${url}`);
console.log(` SOURCE: ${source}`);
console.log('---');
});
if (urlList.length === 0) {
console.log('No URLs or files to test.');
process.exit(0);
}
// Write just the URLs/files to the test_subjects file for Cypress
fs.writeFileSync(URLS_FILE, urlList.map((item) => item.url).join(','));
// Add source information to a separate file for reference during reporting
fs.writeFileSync(
'/tmp/test_subjects_sources.json',
JSON.stringify(urlList, null, 2)
);
// 2. Check if port is in use before starting Hugo
const portInUse = await isPortInUse(HUGO_PORT);
if (portInUse) {
console.log(
`Port ${HUGO_PORT} is already in use. Checking if Hugo is running...`
);
try {
// Try to connect to verify it's a working server
await waitForHugoReady(5000); // Short timeout - if it's running, it should respond quickly
console.log(
`Hugo server already running on port ${HUGO_PORT}, will use existing instance`
);
} catch (err) {
console.error(
`Port ${HUGO_PORT} is in use but not responding as expected: ${err.message}`
);
console.error('Please stop any processes using this port and try again.');
process.exit(1);
}
} else {
// Start Hugo server using the imported function
try {
console.log(
`Starting Hugo server (logs will be written to ${HUGO_LOG_FILE})...`
);
// Create or clear the log file
fs.writeFileSync(HUGO_LOG_FILE, '');
// First, check if Hugo is installed and available
try {
// Try running a simple Hugo version command to check if Hugo is available
const hugoCheck = spawn('hugo', ['version'], { shell: true });
await new Promise((resolve, reject) => {
hugoCheck.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Hugo check failed with code ${code}`));
}
});
hugoCheck.on('error', (err) => reject(err));
});
console.log('Hugo is available on the system');
} catch (checkErr) {
console.log(
'Hugo not found on PATH, will use project-local Hugo via yarn'
);
}
// Use the startHugoServer function from hugo-server.js
hugoProc = await startHugoServer({
configFile: 'config/testing/config.yml',
port: HUGO_PORT,
buildDrafts: true,
noHTTPCache: true,
logFile: HUGO_LOG_FILE,
});
// Ensure hugoProc is a valid process object with kill method
if (!hugoProc || typeof hugoProc.kill !== 'function') {
throw new Error('Failed to get a valid Hugo process object');
}
hugoStarted = true;
console.log(`Started Hugo process with PID: ${hugoProc.pid}`);
// Wait for Hugo to be ready
await waitForHugoReady();
console.log(`Hugo server ready on port ${HUGO_PORT}`);
} catch (err) {
console.error(`Error starting or waiting for Hugo: ${err.message}`);
if (hugoProc && typeof hugoProc.kill === 'function') {
hugoProc.kill('SIGTERM');
}
process.exit(1);
}
}
// 3. Prepare Cypress directories
try {
const screenshotsDir = path.resolve('cypress/screenshots');
const videosDir = path.resolve('cypress/videos');
const specScreenshotDir = path.join(screenshotsDir, 'article-links.cy.js');
// Ensure base directories exist
ensureDirectoryExists(screenshotsDir);
ensureDirectoryExists(videosDir);
// Create spec-specific screenshot directory with a placeholder file
ensureDirectoryExists(specScreenshotDir);
// Create a dummy screenshot file to prevent trash errors
const dummyScreenshotPath = path.join(specScreenshotDir, 'dummy.png');
if (!fs.existsSync(dummyScreenshotPath)) {
// Create a minimal valid PNG file (1x1 transparent pixel)
const minimalPng = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00,
0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]);
fs.writeFileSync(dummyScreenshotPath, minimalPng);
console.log(`Created dummy screenshot file: ${dummyScreenshotPath}`);
}
console.log('Cypress directories prepared successfully');
} catch (err) {
console.warn(
`Warning: Error preparing Cypress directories: ${err.message}`
);
// Continue execution - this is not a fatal error
}
// 4. Run Cypress tests
let cypressFailed = false;
try {
// Initialize/clear broken links report before running tests
console.log('Initializing broken links report...');
initializeReport();
console.log(`Running Cypress tests for ${urlList.length} URLs...`);
const cypressOptions = {
reporter: 'junit',
browser: 'chrome',
config: {
baseUrl: `http://localhost:${HUGO_PORT}`,
video: true,
trashAssetsBeforeRuns: false, // Prevent trash errors
},
env: {
// Pass URLs as a comma-separated string for backward compatibility
test_subjects: urlList.map((item) => item.url).join(','),
// Add new structured data with source information
test_subjects_data: JSON.stringify(urlList),
// Skip testing external links (non-influxdata.com URLs)
skipExternalLinks: true,
},
};
if (specArgs.length > 0) {
console.log(`Using specified test specs: ${specArgs.join(', ')}`);
cypressOptions.spec = specArgs.join(',');
}
const results = await cypress.run(cypressOptions);
// Process broken links report
const brokenLinksCount = displayBrokenLinksReport();
// Determine why tests failed
const testFailureCount = results?.totalFailed || 0;
if (testFailureCount > 0 && brokenLinksCount === 0) {
console.warn(
` Note: ${testFailureCount} test(s) failed but no broken links were detected in the report.`
);
console.warn(
` This usually indicates test errors unrelated to link validation.`
);
// We should not consider special case domains (those with expected errors) as failures
// but we'll still report other test failures
cypressFailed = true;
exitCode = 1;
} else if (brokenLinksCount > 0) {
console.error(
`⚠️ Tests failed: ${brokenLinksCount} broken link(s) detected`
);
cypressFailed = true;
exitCode = 1;
} else if (results) {
console.log('✅ Tests completed successfully');
}
} catch (err) {
console.error(`❌ Cypress execution error: ${err.message}`);
console.error(
`Check Hugo server logs at ${HUGO_LOG_FILE} for any server issues`
);
// Still try to display broken links report if available
displayBrokenLinksReport();
cypressFailed = true;
exitCode = 1;
} finally {
// Stop Hugo server only if we started it
if (hugoProc && hugoStarted && typeof hugoProc.kill === 'function') {
console.log(`Stopping Hugo server (fast shutdown: ${cypressFailed})...`);
if (cypressFailed) {
hugoProc.kill('SIGKILL');
console.log('Hugo server forcibly terminated');
} else {
const shutdownTimeout = setTimeout(() => {
console.error(
'Hugo server did not shut down gracefully, forcing termination'
);
hugoProc.kill('SIGKILL');
process.exit(exitCode);
}, 2000);
hugoProc.kill('SIGTERM');
hugoProc.on('close', () => {
clearTimeout(shutdownTimeout);
console.log('Hugo server shut down successfully');
process.exit(exitCode);
});
// Return to prevent immediate exit
return;
}
} else if (hugoStarted) {
console.log('Hugo process was started but is not available for cleanup');
}
process.exit(exitCode);
}
}
main().catch((err) => {
console.error(`Fatal error: ${err}`);
process.exit(1);
});

View File

@ -1,7 +1,7 @@
influxdb3_core:
name: InfluxDB 3 Core
altname: InfluxDB 3 Core
namespace: influxdb
namespace: influxdb3
menu_category: self-managed
versions: [core]
list_order: 2
@ -16,7 +16,7 @@ influxdb3_core:
influxdb3_enterprise:
name: InfluxDB 3 Enterprise
altname: InfluxDB 3 Enterprise
namespace: influxdb
namespace: influxdb3
menu_category: self-managed
versions: [enterprise]
list_order: 2
@ -76,6 +76,7 @@ influxdb:
name: InfluxDB
altname: InfluxDB OSS
namespace: influxdb
succeeded_by: influxdb3_core
menu_category: self-managed
list_order: 1
placeholder_host: localhost:8086

126
eslint.config.js Normal file
View File

@ -0,0 +1,126 @@
import globals from 'globals';
import jsdocPlugin from 'eslint-plugin-jsdoc';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
import a11yPlugin from 'eslint-plugin-jsx-a11y';
import prettierConfig from 'eslint-config-prettier';
/** @type {import('eslint').Linter.Config[]} */
export default [
// Base configurations
{
languageOptions: {
globals: {
...globals.browser,
// Hugo-specific globals
hugo: 'readonly',
params: 'readonly',
// Common libraries used in docs
Alpine: 'readonly',
CodeMirror: 'readonly',
d3: 'readonly',
},
ecmaVersion: 2022,
sourceType: 'module',
},
},
// JavaScript config (extract rules only)
{
rules: { ...pluginJs.configs.recommended.rules },
},
// TypeScript configurations with proper plugin format
{
plugins: {
'@typescript-eslint': tseslint.plugin,
},
rules: { ...tseslint.configs.recommended.rules },
},
// Import plugin with proper plugin format
{
plugins: {
import: importPlugin,
},
rules: { ...importPlugin.configs.recommended.rules },
},
// Accessibility rules with proper plugin format
{
plugins: {
'jsx-a11y': a11yPlugin,
},
rules: { ...a11yPlugin.configs.recommended.rules },
},
// Add to your config array:
{
plugins: {
jsdoc: jsdocPlugin,
},
rules: {
'jsdoc/require-description': 'warn',
'jsdoc/require-param-description': 'warn',
'jsdoc/require-returns-description': 'warn',
// Add more JSDoc rules as needed
},
},
// Prettier compatibility (extract rules only)
{
rules: { ...prettierConfig.rules },
},
// Custom rules for documentation project
{
rules: {
// Documentation projects often need to use console for examples
'no-console': 'off',
// Module imports
'import/extensions': ['error', 'ignorePackages'],
'import/no-unresolved': 'off', // Hugo handles module resolution differently
// Code formatting
'max-len': ['warn', { code: 80, ignoreUrls: true, ignoreStrings: true }],
quotes: ['error', 'single', { avoidEscape: true }],
// Hugo template string linting (custom rule)
'no-template-curly-in-string': 'off', // Allow ${} in strings for Hugo templates
// Accessibility
'jsx-a11y/anchor-is-valid': 'warn',
},
},
// Configuration for specific file patterns
{
files: ['**/*.js'],
rules: {
// Rules specific to JavaScript files
},
},
{
files: ['assets/js/**/*.js'],
rules: {
// Rules specific to JavaScript in Hugo assets
},
},
{
files: ['**/*.ts'],
rules: {
// Rules specific to TypeScript files
},
},
{
// Ignore rules for build files and external dependencies
ignores: [
'**/node_modules/**',
'**/public/**',
'**/resources/**',
'**/.hugo_build.lock',
],
},
];

View File

@ -1,9 +0,0 @@
import globals from "globals";
import pluginJs from "@eslint/js";
/** @type {import('eslint').Linter.Config[]} */
export default [
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
];

View File

@ -54,3 +54,16 @@ outputFormats:
mediaType: application/json
baseName: pages
isPlainText: true
build:
# Ensure Hugo correctly processes JavaScript modules
jsConfig:
nodeEnv: "development"
module:
mounts:
- source: assets
target: assets
- source: node_modules
target: assets/node_modules

View File

@ -6,6 +6,13 @@
{{ $kapacitorVersion := replaceRE "v" "" .Site.Data.products.kapacitor.latest }}
{{ $fluxVersion := replaceRE "v" "" .Site.Data.products.flux.latest }}
<!--
Show the page if:
- This is a regular page (not test-only) OR
- This is a test-only page BUT we're currently in the testing environment
-->
{{ if or (not .Params.test_only) (and .Params.test_only (in site.Params.environment (slice "testing" "development"))) }}
{{ partial "header.html" . }}
{{ partial "topnav.html" . }}
@ -264,3 +271,9 @@
</div>
</div>
{{ partial "footer.html" . }}
{{ else }}
<!-- Return 404 or empty template for test_only content in production -->
{{ if eq .Params.test_only true }}
{{ template "404.html" . }}
{{ end }}
{{ end }}

View File

@ -2,11 +2,47 @@
{{ $product := index $productPathData 0 }}
{{ $version := index $productPathData 1 | default "0"}}
{{ $productKey := cond (eq $product "influxdb3") (print "influxdb3_" (replaceRE "-" "_" $version)) $product }}
{{ $productName := cond (isset (index .Site.Data.products $productKey) "altname") (index .Site.Data.products $productKey).altname (index .Site.Data.products $productKey).name }}
{{ $stableVersion := (replaceRE `\.[0-9x]+$` "" (index .Site.Data.products $product).latest) }}
{{ $stableVersionURL := replaceRE `v[1-3]` $stableVersion .RelPermalink }}
{{ $stableDefaultURL := print "/" $product "/" $stableVersion "/" }}
<!-- Initialize version variables -->
{{ $successorInfo := dict "exists" false }}
{{ $productName := $product | humanize }}
{{ $stableVersion := "" }}
{{ $stableVersionURL := "" }}
{{ $stableDefaultURL := "" }}
<!-- Get current product name -->
{{ if isset .Site.Data.products $productKey }}
{{ $productName = cond (isset (index .Site.Data.products $productKey) "altname") (index .Site.Data.products $productKey).altname (index .Site.Data.products $productKey).name }}
{{ end }}
<!-- Check for successor and get version information -->
{{ if and (isset .Site.Data.products $productKey) (isset (index .Site.Data.products $productKey) "succeeded_by") }}
{{ $successorKey := (index .Site.Data.products $productKey).succeeded_by }}
{{ if and $successorKey (isset .Site.Data.products $successorKey) }}
<!-- Successor exists and is valid -->
{{ $successorInfo = dict
"exists" true
"key" $successorKey
"name" (cond (isset (index .Site.Data.products $successorKey) "altname")
(index .Site.Data.products $successorKey).altname
(index .Site.Data.products $successorKey).name)
"version" (replaceRE `\.[0-9x]+$` "" (index .Site.Data.products $successorKey).latest)
"namespace" (index .Site.Data.products $successorKey).namespace
}}
<!-- Set stable version to successor version -->
{{ $stableVersion = $successorInfo.version }}
{{ $stableVersionURL = print "/" $successorInfo.namespace "/" $stableVersion "/" }}
{{ $stableDefaultURL = $stableVersionURL }}
{{ end }}
{{ else if isset .Site.Data.products $product }}
<!-- No successor, use current product's latest version -->
{{ $stableVersion = (replaceRE `\.[0-9x]+$` "" (index .Site.Data.products $product).latest) }}
{{ $stableVersionURL = replaceRE `v[1-3]` $stableVersion .RelPermalink }}
{{ $stableDefaultURL = print "/" $product "/" $stableVersion "/" }}
{{ end }}
{{ $stableEquivalentURL := index .Page.Params.alt_links $stableVersion | default "does-not-exist" }}
{{ $stableEquivalentPage := .GetPage (replaceRE `\/$` "" $stableEquivalentURL) }}
{{ $stablePageExists := gt (len $stableEquivalentPage.Title) 0 }}
@ -14,34 +50,32 @@
{{ $isMultiVersion := in (print "/" $version) "/v" }}
{{ if and (in $productWhiteList $product) $isMultiVersion }}
<!-- Check if the current version is less than the stable version -->
{{ if lt (int (replaceRE `[a-z]` "" $version)) (int (replaceRE `[a-z]` "" $stableVersion)) }}
{{ if $successorInfo.exists }}
<!-- Show callout for product with successor -->
<div class="warn block old-version">
<p>
This page documents an earlier version of {{ $productName }}.
<a href="/{{ $product }}/{{ $stableVersion }}/">{{ $productName }} {{ $stableVersion }}</a> is the latest stable version.
<!-- Check if page exists in latest major version docs -->
{{ if gt (len (.GetPage ((replaceRE `v[1-3]` $stableVersion .RelPermalink) | replaceRE `\/$` "")).Title) 0 }}
<a href="{{ $stableVersionURL }}">View this page in the {{ $stableVersion }} documentation</a>.
<!-- Check if the stable equivalent page exists -->
{{ else if $stablePageExists }}
<span style="margin-right:.25rem">See the equivalent <strong>InfluxDB {{ $stableVersion }}</strong> documentation:</span> <a href="{{ $stableEquivalentPage.RelPermalink }}">{{ $stableEquivalentPage.Title | .RenderString }}</a>.
{{ else }}
See the <a href="{{ $stableDefaultURL }}">InfluxDB {{ $stableVersion }} documentation</a>.
{{ end }}
<a href="{{ $stableDefaultURL }}">{{ $successorInfo.name }}</a> is the latest stable version.
</p>
</div>
{{ else if $stableVersion }}
<!-- Show callout for product with newer version (no successor) -->
{{ if lt (int (replaceRE `[a-z]` "" $version)) (int (replaceRE `[a-z]` "" $stableVersion)) }}
<div class="warn block old-version">
<p>
This page documents an earlier version of {{ $productName }}.
<a href="/{{ $product }}/{{ $stableVersion }}/">{{ $productName }} {{ $stableVersion }}</a> is the latest stable version.
<!-- Handle page navigation options -->
{{ if gt (len (.GetPage ((replaceRE `v[1-3]` $stableVersion .RelPermalink) | replaceRE `\/$` "")).Title) 0 }}
<a href="{{ $stableVersionURL }}">View this page in the {{ $stableVersion }} documentation</a>.
{{ else if $stablePageExists }}
<span style="margin-right:.25rem">See the equivalent <strong>{{ $productName }} {{ $stableVersion }}</strong> documentation:</span> <a href="{{ $stableEquivalentPage.RelPermalink }}">{{ $stableEquivalentPage.Title | .RenderString }}</a>.
{{ else }}
See the <a href="{{ $stableDefaultURL }}">{{ $productName }} {{ $stableVersion }} documentation</a>.
{{ end }}
</p>
</div>
{{ end }}
{{ end }}
{{ end }}
{{ if and .Page.Params.v2 (eq (findRE `v[1-3]` $version) (findRE `v[1-3]` $stableVersion)) }}
<div class="note block old-version">
<p>
{{ if $stablePageExists }}
<span style="margin-right:.25rem">See the equivalent <strong>InfluxDB {{ $stableVersion }}</strong> documentation:</span> <a href="{{ $stableEquivalentPage.RelPermalink }}">{{ $stableEquivalentPage.Title | .RenderString }}</a>.
{{ else }}
See the <a href="{{ $stableEquivalentURL }}">equivalent InfluxDB {{ $stableVersion }} documentation</a>.
{{ end }}
</p>
</div>
{{ end }}
{{ end }}

View File

@ -8,7 +8,7 @@
{{ $dateTime := resources.Get "js/datetime.js" }}
{{ $homepageInteractions := resources.Get "js/home-interactions.js" }}
{{ $releaseTOC := resources.Get "/js/release-toc.js" }}
{{ $footerjs := slice $versionSelector $searchInteractions $listFilters $featureCallouts $keybindings $homepageInteractions | resources.Concat "js/footer.bundle.js" | resources.Fingerprint }}
{{ $footerjs := slice $jquery $versionSelector $searchInteractions $listFilters $featureCallouts $keybindings $homepageInteractions | resources.Concat "js/footer.bundle.js" | resources.Fingerprint }}
{{ $fluxGroupKeyjs := $fluxGroupKeys | resources.Fingerprint }}
{{ $dateTimejs := $dateTime | resources.Fingerprint }}
{{ $releaseTOCjs := $releaseTOC | resources.Fingerprint }}

View File

@ -1,13 +1,3 @@
<!-- START COMPONENT AND JS BUNDLING REFACTOR
Eventually, all site-specific JavaScript and external JS
dependencies will be bundled in main.js
-->
<!-- Legacy: keep jquery here until component refactor is for scripts in footer.bundle.js that still require it. -->
{{ $jquery := resources.Get "js/jquery-3.5.0.min.js" }}
{{ $headerjs := slice $jquery | resources.Concat "js/header.bundle.js" | resources.Fingerprint }}
<script type="text/javascript" src="{{ $headerjs.RelPermalink }}"></script>
<!-- $productPathData here is buggy - it might not return the current page path due to the context in which .RelPermalink is called -->
{{ $productPathData := findRE "[^/]+.*?" .RelPermalink }}
{{ $product := index $productPathData 0 }}
@ -23,7 +13,7 @@
{{ $products := .Site.Data.products }}
{{ $influxdb_urls := .Site.Data.influxdb_urls }}
<!-- Build main.js -->
{{ with resources.Get "js/main.js" }}
{{ with resources.Get "js/index.js" }}
{{ $opts := dict
"minify" hugo.IsProduction
"sourceMap" (cond hugo.IsProduction "" "external")

View File

@ -1,135 +1,86 @@
# Refer for explanation to following link:
# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
#
pre-push:
commands:
packages-audit:
tags: frontend security
run: yarn audit
pre-commit:
parallel: true
parallel: true
commands:
# Report linting warnings and errors, don't output files to stdout
lint-markdown:
tags: lint
glob: "content/**/*.md"
glob: 'content/*.md'
run: |
docker compose run --rm --name remark-lint remark-lint '{staged_files}'
cloud-lint:
tags: lint,v2
glob: "content/influxdb/cloud/**/*.md"
glob: 'content/influxdb/cloud/*.md'
run: '.ci/vale/vale.sh
--config=.vale.ini
--minAlertLevel=error {staged_files}'
cloud-dedicated-lint:
tags: lint,v3
glob: "content/influxdb/cloud-dedicated/**/*.md"
glob: 'content/influxdb/cloud-dedicated/*.md'
run: '.ci/vale/vale.sh
--config=content/influxdb/cloud-dedicated/.vale.ini
--minAlertLevel=error {staged_files}'
cloud-serverless-lint:
tags: lint,v3
glob: "content/influxdb/cloud-serverless/**/*.md"
glob: 'content/influxdb/cloud-serverless/*.md'
run: '.ci/vale/vale.sh
--config=content/influxdb/cloud-serverless/.vale.ini
--minAlertLevel=error {staged_files}'
clustered-lint:
tags: lint,v3
glob: "content/influxdb/clustered/**/*.md"
glob: 'content/influxdb/clustered/*.md'
run: '.ci/vale/vale.sh
--config=content/influxdb/cloud-serverless/.vale.ini
--minAlertLevel=error {staged_files}'
telegraf-lint:
tags: lint,clients
glob: "content/telegraf/**/*.md"
tags: lint,clients
glob: 'content/telegraf/*.md'
run: '.ci/vale/vale.sh
--config=.vale.ini
--minAlertLevel=error {staged_files}'
v2-lint:
tags: lint,v2
glob: "content/influxdb/v2/**/*.md"
glob: 'content/influxdb/v2/*.md'
run: '.ci/vale/vale.sh
--config=content/influxdb/v2/.vale.ini
--minAlertLevel=error {staged_files}'
# Link checking for InfluxDB v2
v2-links:
tags: test,links,v2
glob: "content/influxdb/v2/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
# Link checking for InfluxDB v3 core
v3-core-links:
tags: test,links,v3
glob: "content/influxdb3/core/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
# Link checking for InfluxDB v3 enterprise
v3-enterprise-links:
tags: test,links,v3
glob: "content/influxdb3/enterprise/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
# Link checking for Cloud products
cloud-links:
tags: test,links,cloud
glob: "content/influxdb/{cloud,cloud-dedicated,cloud-serverless}/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
# Link checking for Telegraf
telegraf-links:
tags: test,links
glob: "content/telegraf/**/*.{md,html}"
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
cloud-pytest:
glob: content/influxdb/cloud/**/*.md
tags: test,codeblocks,v2
env:
- SERVICE: cloud-pytest
run: docker compose run --rm --name $SERVICE $SERVICE '{staged_files}'
cloud-dedicated-pytest:
tags: test,codeblocks,v3
glob: content/influxdb/cloud-dedicated/**/*.md
env:
- SERVICE: cloud-dedicated-pytest
run: |
./test/scripts/monitor-tests.sh start $SERVICE ;
docker compose run --name $SERVICE $SERVICE {staged_files} ;
./test/scripts/monitor-tests.sh stop $SERVICE
cloud-serverless-pytest:
tags: test,codeblocks,v3
glob: content/influxdb/cloud-serverless/**/*.md
env:
- SERVICE: cloud-serverless-pytest
run: docker compose run --rm --name $SERVICE $SERVICE '{staged_files}'
clustered-pytest:
tags: test,codeblocks,v3
glob: content/influxdb/clustered/**/*.md
env:
- SERVICE: clustered-pytest
run: |
./test/scripts/monitor-tests.sh start $SERVICE ;
docker compose run --name $SERVICE $SERVICE {staged_files} ;
./test/scripts/monitor-tests.sh stop $SERVICE
telegraf-pytest:
tags: test,codeblocks
glob: content/telegraf/**/*.md
env:
- SERVICE: telegraf-pytest
run: docker compose run --rm --name $SERVICE $SERVICE '{staged_files}'
v2-pytest:
tags: test,codeblocks,v2
glob: content/influxdb/v2/**/*.md
env:
- SERVICE: v2-pytest
run: docker compose run --rm --name $SERVICE $SERVICE '{staged_files}'
prettier:
tags: frontend,style
glob: "*.{css,js,ts,jsx,tsx}"
run: yarn prettier {staged_files}
build:
tags: [frontend, style]
glob: '*.{css,js,ts,jsx,tsx}'
run: |
yarn prettier --write --loglevel silent "{staged_files}" > /dev/null 2>&1 ||
{ echo "⚠️ Prettier found formatting issues. Automatic formatting applied."
git add {staged_files}
}
pre-push:
commands:
packages-audit:
tags: frontend security
run: yarn audit
e2e-shortcode-examples:
tags: [frontend, test]
glob:
- assets/*.{js,mjs,css,scss}
- layouts/*.html
- content/example.md
files: /bin/ls content/example.md
run: |
node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/article-links.cy.js" {files}
exit $?
e2e-links:
tags: test,links
glob: 'content/**/*.{md,html}'
run: |
echo "Running link checker for: {staged_files}"
yarn test:links {staged_files}
exit $?
# Manage Docker containers
prune-legacy-containers:
priority: 1
tags: test
@ -137,6 +88,48 @@ build:
--filter label=tag=influxdata-docs
--filter status=exited | xargs docker rm)
|| true'
rebuild-test-images:
build-pytest-image:
tags: test
run: docker compose build pytest-codeblocks
run: yarn build:pytest:image
# Test code blocks in markdown files
cloud-pytest:
glob: content/influxdb/cloud/**/*.md
tags: test,codeblocks,v2
env:
SERVICE: cloud-pytest
run: yarn test:codeblocks:cloud '{staged_files}'
cloud-dedicated-pytest:
tags: test,codeblocks,v3
glob: content/influxdb/cloud-dedicated/**/*.md
run: |
yarn test:codeblocks:cloud-dedicated '{staged_files}' &&
./test/scripts/monitor-tests.sh stop cloud-dedicated-pytest
cloud-serverless-pytest:
tags: test,codeblocks,v3
glob: content/influxdb/cloud-serverless/**/*.md
env:
SERVICE: cloud-serverless-pytest
run: yarn test:codeblocks:cloud-serverless '{staged_files}'
clustered-pytest:
tags: test,codeblocks,v3
glob: content/influxdb/clustered/**/*.md
run: |
yarn test:codeblocks:clustered '{staged_files}' &&
./test/scripts/monitor-tests.sh stop clustered-pytest
telegraf-pytest:
tags: test,codeblocks
glob: content/telegraf/**/*.md
env:
SERVICE: telegraf-pytest
run: yarn test:codeblocks:telegraf '{staged_files}'
v2-pytest:
tags: test,codeblocks,v2
glob: content/influxdb/v2/**/*.md
env:
SERVICE: v2-pytest
run: yarn test:codeblocks:v2 '{staged_files}'

View File

@ -14,16 +14,23 @@
"autoprefixer": ">=10.2.5",
"cypress": "^14.0.1",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsdoc": "^50.6.17",
"eslint-plugin-jsx-a11y": "^6.10.2",
"globals": "^15.14.0",
"hugo-extended": ">=0.101.0",
"postcss": ">=8.4.31",
"postcss-cli": ">=9.1.0",
"prettier": "^3.2.5",
"prettier-plugin-sql": "^0.18.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"winston": "^3.16.0"
},
"dependencies": {
"axios": "^1.7.4",
"gray-matter": "^4.0.3",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"js-yaml": "^4.1.0",
@ -33,16 +40,42 @@
"vanillajs-datepicker": "^1.3.4"
},
"scripts": {
"e2e:chrome": "npx cypress run --browser chrome",
"e2e:o": "npx cypress open",
"e2e:o:links": "node cypress/support/map-files-to-urls.mjs content/influxdb3/core/get-started/_index.md --debug",
"e2e:api-docs": "export cypress_test_subjects=\"http://localhost:1313/influxdb3/core/api/,http://localhost:1313/influxdb3/enterprise/api/,http://localhost:1313/influxdb3/cloud-dedicated/api/,http://localhost:1313/influxdb3/cloud-dedicated/api/v1/,http://localhost:1313/influxdb/cloud-dedicated/api/v1/,http://localhost:1313/influxdb/cloud-dedicated/api/management/,http://localhost:1313/influxdb3/cloud-dedicated/api/management/\"; npx cypress run --spec cypress/e2e/article-links.cy.js",
"build:pytest:image": "docker build -t influxdata/docs-pytest:latest -f Dockerfile.pytest .",
"lint": "LEFTHOOK_EXCLUDE=test lefthook run pre-commit && lefthook run pre-push",
"pre-commit": "lefthook run pre-commit",
"test-content": "docker compose --profile test up"
"test": "echo \"Run 'yarn test:e2e', 'yarn test:links', 'yarn test:codeblocks:all' or a specific test command. e2e and links test commands can take a glob of file paths to test. Some commands run automatically during the git pre-commit and pre-push hooks.\" && exit 0",
"test:codeblocks": "echo \"Run a specific codeblocks test command\" && exit 0",
"test:codeblocks:all": "docker compose --profile test up",
"test:codeblocks:cloud": "docker compose run --rm --name cloud-pytest cloud-pytest",
"test:codeblocks:cloud-dedicated": "./test/scripts/monitor-tests.sh start cloud-dedicated-pytest && docker compose run --name cloud-dedicated-pytest cloud-dedicated-pytest",
"test:codeblocks:cloud-serverless": "docker compose run --rm --name cloud-serverless-pytest cloud-serverless-pytest",
"test:codeblocks:clustered": "./test/scripts/monitor-tests.sh start clustered-pytest && docker compose run --name clustered-pytest clustered-pytest",
"test:codeblocks:telegraf": "docker compose run --rm --name telegraf-pytest telegraf-pytest",
"test:codeblocks:v2": "docker compose run --rm --name v2-pytest v2-pytest",
"test:codeblocks:stop-monitors": "./test/scripts/monitor-tests.sh stop cloud-dedicated-pytest && ./test/scripts/monitor-tests.sh stop clustered-pytest",
"test:e2e": "node cypress/support/run-e2e-specs.js",
"test:links": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\"",
"test:links:v1": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb/{v1,enterprise_influxdb}/**/*.{md,html}",
"test:links:v2": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb/{cloud,v2}/**/*.{md,html}",
"test:links:v3": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb3/**/*.{md,html}",
"test:links:chronograf": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/chronograf/**/*.{md,html}",
"test:links:kapacitor": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/kapacitor/**/*.{md,html}",
"test:links:telegraf": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/telegraf/**/*.{md,html}",
"test:links:shared": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/shared/**/*.{md,html}",
"test:links:api-docs": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" /influxdb3/core/api/,/influxdb3/enterprise/api/,/influxdb3/cloud-dedicated/api/,/influxdb3/cloud-dedicated/api/v1/,/influxdb/cloud-dedicated/api/v1/,/influxdb/cloud-dedicated/api/management/,/influxdb3/cloud-dedicated/api/management/",
"test:shortcode-examples": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/example.md"
},
"main": "assets/js/main.js",
"module": "assets/js/main.js",
"type": "module",
"browserslist": [
"last 2 versions",
"not dead",
"not IE 11"
],
"engines": {
"node": ">=16.0.0"
},
"main": "index.js",
"module": "main.js",
"directories": {
"test": "test"
},

1289
yarn.lock

File diff suppressed because it is too large Load Diff