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"