diff --git a/package.json b/package.json
index 324885c1..34f3b0ab 100644
--- a/package.json
+++ b/package.json
@@ -67,7 +67,6 @@
"dependencies": {
"@emotion/css": "^11.1.3",
"@grafana/data": "9.4.3",
- "@grafana/experimental": "^1.7.0",
"@grafana/runtime": "9.4.3",
"@grafana/ui": "10.1.0",
"js-sql-parser": "^1.6.0",
@@ -75,6 +74,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^5.2.0",
+ "semver": "7.6.2",
"tslib": "^2.5.3"
},
"packageManager": "yarn@1.22.19"
diff --git a/src/components/configEditor/AliasTableConfig.tsx b/src/components/configEditor/AliasTableConfig.tsx
index d5fdb8ce..7a719bb8 100644
--- a/src/components/configEditor/AliasTableConfig.tsx
+++ b/src/components/configEditor/AliasTableConfig.tsx
@@ -1,5 +1,5 @@
import React, {ChangeEvent, useState} from 'react';
-import {ConfigSection} from '@grafana/experimental';
+import {ConfigSection} from 'components/experimental/ConfigSection';
import {Input, Field, HorizontalGroup, Button} from '@grafana/ui';
import {AliasTableEntry} from 'types/config';
import allLabels from 'labels';
diff --git a/src/components/configEditor/DefaultDatabaseTableConfig.tsx b/src/components/configEditor/DefaultDatabaseTableConfig.tsx
index 2ac2221b..f590e086 100644
--- a/src/components/configEditor/DefaultDatabaseTableConfig.tsx
+++ b/src/components/configEditor/DefaultDatabaseTableConfig.tsx
@@ -1,5 +1,5 @@
import React, { SyntheticEvent } from 'react';
-import { ConfigSection } from '@grafana/experimental';
+import { ConfigSection } from 'components/experimental/ConfigSection';
import { Input, Field } from '@grafana/ui';
import allLabels from 'labels';
diff --git a/src/components/configEditor/HttpHeadersConfig.tsx b/src/components/configEditor/HttpHeadersConfig.tsx
index cdba016b..cec992bf 100644
--- a/src/components/configEditor/HttpHeadersConfig.tsx
+++ b/src/components/configEditor/HttpHeadersConfig.tsx
@@ -1,5 +1,5 @@
import React, { ChangeEvent, useMemo, useState } from 'react';
-import { ConfigSection } from '@grafana/experimental';
+import { ConfigSection } from 'components/experimental/ConfigSection';
import { Input, Field, HorizontalGroup, Switch, SecretInput, Button } from '@grafana/ui';
import { CHHttpHeader } from 'types/config';
import allLabels from 'labels';
diff --git a/src/components/configEditor/LogsConfig.tsx b/src/components/configEditor/LogsConfig.tsx
index ea4a26ed..c1caffad 100644
--- a/src/components/configEditor/LogsConfig.tsx
+++ b/src/components/configEditor/LogsConfig.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { ConfigSection, ConfigSubSection } from '@grafana/experimental';
+import { ConfigSection, ConfigSubSection } from 'components/experimental/ConfigSection';
import { Input, Field } from '@grafana/ui';
import { OtelVersionSelect } from 'components/queryBuilder/OtelVersionSelect';
import { ColumnHint } from 'types/queryBuilder';
diff --git a/src/components/configEditor/QuerySettingsConfig.tsx b/src/components/configEditor/QuerySettingsConfig.tsx
index 1a096965..98e0bbb1 100644
--- a/src/components/configEditor/QuerySettingsConfig.tsx
+++ b/src/components/configEditor/QuerySettingsConfig.tsx
@@ -1,6 +1,6 @@
import React, { FormEvent } from 'react';
import { Switch, Input, Field } from '@grafana/ui';
-import { ConfigSection } from '@grafana/experimental';
+import { ConfigSection } from 'components/experimental/ConfigSection';
import allLabels from 'labels';
interface QuerySettingsConfigProps {
diff --git a/src/components/configEditor/TracesConfig.tsx b/src/components/configEditor/TracesConfig.tsx
index b365a2ee..719c87d8 100644
--- a/src/components/configEditor/TracesConfig.tsx
+++ b/src/components/configEditor/TracesConfig.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { ConfigSection, ConfigSubSection } from '@grafana/experimental';
+import { ConfigSection, ConfigSubSection } from 'components/experimental/ConfigSection';
import { Input, Field } from '@grafana/ui';
import { OtelVersionSelect } from 'components/queryBuilder/OtelVersionSelect';
import { ColumnHint, TimeUnit } from 'types/queryBuilder';
diff --git a/src/components/experimental/ConfigSection/ConfigSection.test.tsx b/src/components/experimental/ConfigSection/ConfigSection.test.tsx
new file mode 100644
index 00000000..bb42f576
--- /dev/null
+++ b/src/components/experimental/ConfigSection/ConfigSection.test.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import { ConfigSection } from './ConfigSection';
+
+describe('', () => {
+ it('should render title as
', () => {
+ render(
+
+ Content
+
+ );
+
+ expect(screen.getByText('Test title').tagName).toBe('H3');
+ });
+});
diff --git a/src/components/experimental/ConfigSection/ConfigSection.tsx b/src/components/experimental/ConfigSection/ConfigSection.tsx
new file mode 100644
index 00000000..c31e378c
--- /dev/null
+++ b/src/components/experimental/ConfigSection/ConfigSection.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { GenericConfigSection, Props as GenericConfigSectionProps } from './GenericConfigSection';
+
+type Props = Omit;
+
+export const ConfigSection = ({ children, ...props }: Props) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/experimental/ConfigSection/ConfigSubSection.test.tsx b/src/components/experimental/ConfigSection/ConfigSubSection.test.tsx
new file mode 100644
index 00000000..05660ce9
--- /dev/null
+++ b/src/components/experimental/ConfigSection/ConfigSubSection.test.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import { ConfigSubSection } from './ConfigSubSection';
+
+describe('', () => {
+ it('should render title as ', () => {
+ render(
+
+ Content
+
+ );
+
+ expect(screen.getByText('Test title').tagName).toBe('H6');
+ });
+});
diff --git a/src/components/experimental/ConfigSection/ConfigSubSection.tsx b/src/components/experimental/ConfigSection/ConfigSubSection.tsx
new file mode 100644
index 00000000..222dfa6a
--- /dev/null
+++ b/src/components/experimental/ConfigSection/ConfigSubSection.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { GenericConfigSection, Props as GenericConfigSectionProps } from './GenericConfigSection';
+
+type Props = Omit;
+
+export const ConfigSubSection = ({ children, ...props }: Props) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/experimental/ConfigSection/DataSourceDescription.test.tsx b/src/components/experimental/ConfigSection/DataSourceDescription.test.tsx
new file mode 100644
index 00000000..acc6a0a3
--- /dev/null
+++ b/src/components/experimental/ConfigSection/DataSourceDescription.test.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { DataSourceDescription } from './DataSourceDescription';
+import { render } from '@testing-library/react';
+
+describe('', () => {
+ it('should render data source name', () => {
+ const dataSourceName = 'Test data source name';
+ const { getByText } = render(
+
+ );
+
+ expect(getByText(dataSourceName, { exact: false })).toBeInTheDocument();
+ });
+
+ it('should render docs link', () => {
+ const docsLink = 'https://grafana.com/test-datasource-docs';
+ const { getByText } = render(
+
+ );
+
+ const docsLinkEl = getByText('view the documentation');
+
+ expect(docsLinkEl.getAttribute('href')).toBe(docsLink);
+ });
+
+ it('should render text about required fields by default', () => {
+ const { getByText } = render(
+
+ );
+
+ expect(getByText('Fields marked with', { exact: false })).toBeInTheDocument();
+ });
+
+ it('should not render text about required fields when `hasRequiredFields` props is `false`', () => {
+ const { getByText } = render(
+
+ );
+
+ expect(() => getByText('Fields marked with', { exact: false })).toThrow();
+ });
+
+ it('should render passed `className`', () => {
+ const { container } = render(
+
+ );
+
+ expect(container.firstChild).toHaveClass('test-class-name');
+ });
+});
diff --git a/src/components/experimental/ConfigSection/DataSourceDescription.tsx b/src/components/experimental/ConfigSection/DataSourceDescription.tsx
new file mode 100644
index 00000000..710eebb7
--- /dev/null
+++ b/src/components/experimental/ConfigSection/DataSourceDescription.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { cx, css } from '@emotion/css';
+import { useTheme2 } from '@grafana/ui';
+
+type Props = {
+ dataSourceName: string;
+ docsLink: string;
+ hasRequiredFields?: boolean;
+ className?: string;
+};
+
+export const DataSourceDescription = ({ dataSourceName, docsLink, hasRequiredFields = true, className }: Props) => {
+ const theme = useTheme2();
+
+ const styles = {
+ container: css({
+ p: {
+ margin: 0,
+ },
+ 'p + p': {
+ marginTop: theme.spacing(2),
+ },
+ }),
+ text: css({
+ ...theme.typography.body,
+ color: theme.colors.text.secondary,
+ a: css({
+ color: theme.colors.text.link,
+ textDecoration: 'underline',
+ '&:hover': {
+ textDecoration: 'none',
+ },
+ }),
+ }),
+ };
+
+ return (
+
+
+ Before you can use the {dataSourceName} data source, you must configure it below or in the config file. For
+ detailed instructions,{' '}
+
+ view the documentation
+
+ .
+
+ {hasRequiredFields && (
+
+ Fields marked with * are required
+
+ )}
+
+ );
+};
diff --git a/src/components/experimental/ConfigSection/GenericConfigSection.test.tsx b/src/components/experimental/ConfigSection/GenericConfigSection.test.tsx
new file mode 100644
index 00000000..f8b0c8d0
--- /dev/null
+++ b/src/components/experimental/ConfigSection/GenericConfigSection.test.tsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { GenericConfigSection } from './GenericConfigSection';
+
+let user = userEvent.setup();
+
+describe('', () => {
+ beforeEach(() => {
+ userEvent.setup();
+ });
+
+ it('should render title', () => {
+ render(
+
+ Content
+
+ );
+
+ expect(screen.getByText('Test title')).toBeInTheDocument();
+ });
+
+ it('should render title as by default', () => {
+ render(
+
+ Content
+
+ );
+
+ expect(screen.getByText('Test title').tagName).toBe('H3');
+ });
+
+ it('should render title as when `kind` is `section`', () => {
+ render(
+
+ Content
+
+ );
+
+ expect(screen.getByText('Test title').tagName).toBe('H3');
+ });
+
+ it('should render title as when `kind` is `sub-section`', () => {
+ render(
+
+ Content
+
+ );
+
+ expect(screen.getByText('Test title').tagName).toBe('H6');
+ });
+
+ it('should render description', () => {
+ render(
+
+ Content
+
+ );
+
+ expect(screen.getByText('Test description')).toBeInTheDocument();
+ });
+
+ it('should not be collapsible by default', () => {
+ render(
+
+ Test content
+
+ );
+
+ expect(screen.getByText('Test content')).toBeInTheDocument();
+ expect(() => screen.getByLabelText('Expand section Test title')).toThrow();
+ expect(() => screen.getByLabelText('Collapse section Test title')).toThrow();
+ });
+
+ it('should be collapsible with content visible when `isCollapsible` is `true` and `isInitiallyOpen` is not passed', async () => {
+ render(
+
+ Test content
+
+ );
+
+ expect(screen.getByText('Test content')).toBeInTheDocument();
+
+ await user.click(screen.getByLabelText('Collapse section Test title'));
+
+ expect(() => screen.getByText('Test content')).toThrow();
+ });
+
+ it('should be collapsible with content visible when `isCollapsible` is `true` and `isInitiallyOpen` is `true`', async () => {
+ render(
+
+ Test content
+
+ );
+
+ expect(screen.getByText('Test content')).toBeInTheDocument();
+
+ await user.click(screen.getByLabelText('Collapse section Test title'));
+
+ expect(() => screen.getByText('Test content')).toThrow();
+ });
+
+ it('should be collapsible with content hidden when `isCollapsible` is `true` and `isInitiallyOpen` is `false`', async () => {
+ render(
+
+ Test content
+
+ );
+
+ expect(() => screen.getByText('Test content')).toThrow();
+
+ await user.click(screen.getByLabelText('Expand section Test title'));
+
+ expect(screen.getByText('Test content')).toBeInTheDocument();
+ });
+
+ it('should have passed `className`', () => {
+ const { container } = render(
+
+ Test content
+
+ );
+
+ expect(container.firstChild).toHaveClass('test-class');
+ });
+});
diff --git a/src/components/experimental/ConfigSection/GenericConfigSection.tsx b/src/components/experimental/ConfigSection/GenericConfigSection.tsx
new file mode 100644
index 00000000..50b199c5
--- /dev/null
+++ b/src/components/experimental/ConfigSection/GenericConfigSection.tsx
@@ -0,0 +1,72 @@
+import React, { useState, ReactNode } from 'react';
+import { css } from '@emotion/css';
+import { useTheme2, IconButton, IconName } from '@grafana/ui';
+
+export type Props = {
+ title: string;
+ description?: ReactNode;
+ isCollapsible?: boolean;
+ isInitiallyOpen?: boolean;
+ kind?: 'section' | 'sub-section';
+ className?: string;
+ children: ReactNode;
+};
+
+export const GenericConfigSection = ({
+ children,
+ title,
+ description,
+ isCollapsible = false,
+ isInitiallyOpen = true,
+ kind = 'section',
+ className,
+}: Props) => {
+ const { colors, typography, spacing } = useTheme2();
+ const [isOpen, setIsOpen] = useState(isCollapsible ? isInitiallyOpen : true);
+ const iconName: IconName = isOpen ? 'angle-up' : 'angle-down';
+ const isSubSection = kind === 'sub-section';
+ const collapsibleButtonAriaLabel = `${isOpen ? 'Collapse' : 'Expand'} section ${title}`;
+
+ const styles = {
+ header: css({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ }),
+ title: css({
+ margin: 0,
+ }),
+ subtitle: css({
+ margin: 0,
+ fontWeight: typography.fontWeightRegular,
+ }),
+ descriptionText: css({
+ marginTop: spacing(isSubSection ? 0.25 : 0.5),
+ marginBottom: 0,
+ ...typography.bodySmall,
+ color: colors.text.secondary,
+ }),
+ content: css({
+ marginTop: spacing(2),
+ }),
+ };
+
+ return (
+
+
+ {kind === 'section' ?
{title}
: {title}
}
+ {isCollapsible && (
+ setIsOpen(!isOpen)}
+ type="button"
+ size="xl"
+ aria-label={collapsibleButtonAriaLabel}
+ />
+ )}
+
+ {description &&
{description}
}
+ {isOpen &&
{children}
}
+
+ );
+};
diff --git a/src/components/experimental/ConfigSection/index.ts b/src/components/experimental/ConfigSection/index.ts
new file mode 100644
index 00000000..311f916e
--- /dev/null
+++ b/src/components/experimental/ConfigSection/index.ts
@@ -0,0 +1,3 @@
+export { ConfigSection } from './ConfigSection';
+export { ConfigSubSection } from './ConfigSubSection';
+export { DataSourceDescription } from './DataSourceDescription'
diff --git a/src/views/CHConfigEditor.tsx b/src/views/CHConfigEditor.tsx
index eec0ddea..4a1af069 100644
--- a/src/views/CHConfigEditor.tsx
+++ b/src/views/CHConfigEditor.tsx
@@ -16,7 +16,7 @@ import {
AliasTableEntry
} from 'types/config';
import { gte as versionGte } from 'semver';
-import { ConfigSection, ConfigSubSection, DataSourceDescription } from '@grafana/experimental';
+import { ConfigSection, ConfigSubSection, DataSourceDescription } from 'components/experimental/ConfigSection';
import { config } from '@grafana/runtime';
import { Divider } from 'components/Divider';
import { TimeUnit } from 'types/queryBuilder';
diff --git a/yarn.lock b/yarn.lock
index af7fc360..5061df55 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1757,15 +1757,6 @@
eslint-plugin-react-hooks "4.6.0"
typescript "4.8.4"
-"@grafana/experimental@^1.7.0":
- version "1.7.4"
- resolved "https://registry.yarnpkg.com/@grafana/experimental/-/experimental-1.7.4.tgz#5b5efe89abf38b1d3358251148d42b9111de539e"
- integrity sha512-uYkv9HRx+cqJRktsY43ApG0+kgG4fNR8lv+bkaFvGyCg46dcTeNqokujoPnHp6lt9MEn+0Y3jKEQbvCXjcz2bA==
- dependencies:
- "@types/uuid" "^8.3.3"
- semver "^7.5.4"
- uuid "^8.3.2"
-
"@grafana/faro-core@^1.0.0-beta2", "@grafana/faro-core@^1.1.0":
version "1.2.7"
resolved "https://registry.yarnpkg.com/@grafana/faro-core/-/faro-core-1.2.7.tgz#4bad7b12394d866d233b1925fb09857f2bb3381a"
@@ -3416,11 +3407,6 @@
dependencies:
source-map "^0.6.1"
-"@types/uuid@^8.3.3":
- version "8.3.4"
- resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
- integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
-
"@types/webpack-livereload-plugin@^2.3.6":
version "2.3.6"
resolved "https://registry.yarnpkg.com/@types/webpack-livereload-plugin/-/webpack-livereload-plugin-2.3.6.tgz#2c3ccefc8858525f40aeb8be0f784d5027144e23"
@@ -9989,6 +9975,11 @@ selection-is-backward@^1.0.0:
resolved "https://registry.yarnpkg.com/selection-is-backward/-/selection-is-backward-1.0.0.tgz#97a54633188a511aba6419fc5c1fa91b467e6be1"
integrity sha512-C+6PCOO55NLCfS8uQjUKV/6E5XMuUcfOVsix5m0QqCCCKi495NgeQVNfWtAaD71NKHsdmFCJoXUGfir3qWdr9A==
+semver@7.6.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2:
+ version "7.6.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
+ integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
+
semver@^5.3.0, semver@^5.5.0:
version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
@@ -9999,11 +9990,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2:
- version "7.6.2"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
- integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
-
serialize-javascript@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"