Merge branch 'master' into feat-oss-2.7.12-release
commit
f8f48d451a
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" "$@"
|
|
@ -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).
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins:
|
||||
- './../openapi/plugins/docs-plugin.js'
|
||||
- './../openapi/plugins/docs-plugin.cjs'
|
||||
extends:
|
||||
- recommended
|
||||
- docs/all
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins:
|
||||
- '../../openapi/plugins/docs-plugin.js'
|
||||
- '../../openapi/plugins/docs-plugin.cjs'
|
||||
extends:
|
||||
- recommended
|
||||
- docs/all
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins:
|
||||
- '../../openapi/plugins/docs-plugin.js'
|
||||
- '../../openapi/plugins/docs-plugin.cjs'
|
||||
extends:
|
||||
- recommended
|
||||
- docs/all
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins:
|
||||
- '../../openapi/plugins/docs-plugin.js'
|
||||
- '../../openapi/plugins/docs-plugin.cjs'
|
||||
extends:
|
||||
- recommended
|
||||
- docs/all
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins:
|
||||
- '../../openapi/plugins/docs-plugin.js'
|
||||
- '../../openapi/plugins/docs-plugin.cjs'
|
||||
extends:
|
||||
- recommended
|
||||
- docs/all
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins:
|
||||
- '../../openapi/plugins/docs-plugin.js'
|
||||
- '../../openapi/plugins/docs-plugin.cjs'
|
||||
extends:
|
||||
- recommended
|
||||
- docs/all
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins:
|
||||
- '../../openapi/plugins/docs-plugin.js'
|
||||
- '../../openapi/plugins/docs-plugin.cjs'
|
||||
extends:
|
||||
- recommended
|
||||
- docs/all
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
plugins:
|
||||
- '../../openapi/plugins/docs-plugin.js'
|
||||
- '../../openapi/plugins/docs-plugin.cjs'
|
||||
extends:
|
||||
- recommended
|
||||
- docs/all
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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());
|
|
@ -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';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './main.js';
|
|
@ -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;
|
||||
|
||||
// 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.
|
||||
return window.influxdatadocs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
/**
|
||||
* Main initialization function
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
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 };
|
|
@ -0,0 +1,2 @@
|
|||
import:
|
||||
- hugo.yml
|
|
@ -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
|
|
@ -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**_.
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
-->
|
|
@ -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
|
||||
-->
|
|
@ -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
|
||||
-->
|
|
@ -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
|
||||
-->
|
|
@ -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 >}}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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.
|
||||
|
||||
Environment variable: `INFLUXDB3_DATABASE_NAME`
|
||||
- **`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.
|
||||
|
||||
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-->
|
||||
|
||||
|
|
|
@ -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,8 +17,7 @@ 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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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 you’ve 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.
|
|
@ -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 -->
|
|
@ -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 %}}
|
||||
|
|
|
@ -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
|
||||
|
@ -19,14 +21,45 @@ influxdb3 create token <COMMAND> [OPTIONS]
|
|||
## Options
|
||||
|
||||
| Option | | Description |
|
||||
| :----- | :----------- | :------------------------------ |
|
||||
| :----- | :------- | :--------------------- |
|
||||
| |`--admin`| Create an admin token |
|
||||
| `-h` | `--help` | Print help information |
|
||||
| | `--help-all` | Print detailed 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
|
||||
> ```
|
||||
|
|
|
@ -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 %}}
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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/).
|
||||
|
||||
|
@ -373,13 +398,17 @@ For more information, see how to [Manage resource tokens](/influxdb3/version/adm
|
|||
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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
return config;
|
||||
|
||||
// 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);
|
||||
});
|
||||
},
|
||||
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
|
||||
supportFile: 'cypress/support/e2e.js',
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
},
|
||||
env: {
|
||||
test_subjects: '',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,79 +51,115 @@ 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');
|
||||
const linkText = $a.text().trim();
|
||||
|
||||
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
|
||||
|
@ -127,48 +169,74 @@ describe('Article links', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Use DOM to check if the element exists, but don't fail if missing
|
||||
// Use DOM to check if the element exists
|
||||
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);
|
||||
if (!element) {
|
||||
cy.task('reportBrokenLink', {
|
||||
url: `#${anchorId}`,
|
||||
status: 404,
|
||||
type: 'anchor',
|
||||
linkText,
|
||||
page: subject,
|
||||
});
|
||||
cy.log(`⚠️ Missing anchor target: #${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 () {
|
||||
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
|
||||
// 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^="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);
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -14,4 +14,4 @@
|
|||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import './commands';
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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}`));
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
];
|
13
hugo.yml
13
hugo.yml
|
@ -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
|
|
@ -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 }}
|
|
@ -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 $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="{{ $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.
|
||||
<!-- Check if page exists in latest major version docs -->
|
||||
|
||||
<!-- 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>.
|
||||
<!-- 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>.
|
||||
<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 }}">InfluxDB {{ $stableVersion }} documentation</a>.
|
||||
See the <a href="{{ $stableDefaultURL }}">{{ $productName }} {{ $stableVersion }} documentation</a>.
|
||||
{{ end }}
|
||||
</p>
|
||||
</div>
|
||||
{{ 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 }}
|
|
@ -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 }}
|
||||
|
|
|
@ -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")
|
||||
|
|
177
lefthook.yml
177
lefthook.yml
|
@ -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
|
||||
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"
|
||||
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}'
|
47
package.json
47
package.json
|
@ -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"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue