Skip to content

Commit

Permalink
Update Config editor
Browse files Browse the repository at this point in the history
- Replace legacy components
- Update tests
- Add version utils
- Add grafana/experimental
  • Loading branch information
aangelisc committed Sep 7, 2023
1 parent efae33f commit 79519c0
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 103 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"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.1",
"lodash": "^4.17.21",
Expand Down
15 changes: 15 additions & 0 deletions src/components/Divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { Divider as GrafanaDivider, useTheme2 } from '@grafana/ui';
import { config } from '@grafana/runtime';
import { isVersionGtOrEq } from '../utils/version';

export function Divider() {
const theme = useTheme2();
return isVersionGtOrEq(config.buildInfo.version, '10.1.0') ? (
<GrafanaDivider />
) : (
<div
style={{ borderTop: `1px solid ${theme.colors.border.weak}`, margin: theme.spacing(2, 0), width: '100%' }}
></div>
);
}
50 changes: 27 additions & 23 deletions src/editors/SentryConfigEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import React from 'react';
import { DataSourceSettings } from '@grafana/data';
import { render, within } from '@testing-library/react';
import { render, within, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SentryConfigEditor } from './SentryConfigEditor';
import { DEFAULT_SENTRY_URL } from './../constants';
import { selectors } from './../selectors';
import { Components, selectors } from './../selectors';
import type { SentryConfig, SentrySecureConfig } from './../types';

describe('SentryConfigEditor', () => {
it('render default editor without error', () => {
const options = {} as DataSourceSettings<SentryConfig, SentrySecureConfig>;
const options = { jsonData: {} } as DataSourceSettings<SentryConfig, SentrySecureConfig>;
const onOptionsChange = jest.fn();
const result = render(<SentryConfigEditor options={options} onOptionsChange={onOptionsChange} />);
expect(result.container.firstChild).not.toBeNull();
expect(result.getByTestId('sentry-config-editor-url-row')).toBeInTheDocument();
expect(within(result.getByTestId('sentry-config-editor-url-row')).getByTestId('sentry-config-editor-url')).toBeInTheDocument();
expect(within(result.getByTestId('sentry-config-editor-url-row')).getByDisplayValue(DEFAULT_SENTRY_URL)).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-org-slug-row')).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-url')).toBeInTheDocument();
expect(screen.getByPlaceholderText(Components.ConfigEditor.SentrySettings.URL.placeholder)).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-org-slug')).toBeInTheDocument();
expect(screen.getByPlaceholderText(Components.ConfigEditor.SentrySettings.OrgSlug.placeholder)).toBeInTheDocument();

expect(result.getByTestId('sentry-config-editor-auth-token')).toBeInTheDocument();
expect(
within(result.getByTestId('sentry-config-editor-org-slug-row')).getByTestId('sentry-config-editor-org-slug')
).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-auth-token-row')).toBeInTheDocument();
expect(within(result.getByTestId('sentry-config-editor-auth-token-row')).queryByDisplayValue('Configured')).not.toBeInTheDocument();
within(result.getByTestId('sentry-config-editor-auth-token')).queryByDisplayValue('Configured')
).not.toBeInTheDocument();
expect(
within(result.getByTestId('sentry-config-editor-auth-token-row')).queryByText(
within(result.getByTestId('sentry-config-editor-auth-token')).queryByText(
selectors.components.ConfigEditor.SentrySettings.AuthToken.Reset.label
)
).not.toBeInTheDocument();
Expand All @@ -36,25 +36,29 @@ describe('SentryConfigEditor', () => {
const onOptionsChange = jest.fn();
const result = render(<SentryConfigEditor options={options} onOptionsChange={onOptionsChange} />);
expect(result.container.firstChild).not.toBeNull();
expect(result.getByTestId('sentry-config-editor-url-row')).toBeInTheDocument();
expect(within(result.getByTestId('sentry-config-editor-url-row')).getByTestId('sentry-config-editor-url')).toBeInTheDocument();
expect(within(result.getByTestId('sentry-config-editor-url-row')).queryByDisplayValue(DEFAULT_SENTRY_URL)).not.toBeInTheDocument();
expect(within(result.getByTestId('sentry-config-editor-url-row')).getByDisplayValue('https://foo.com')).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-org-slug-row')).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-url')).toBeInTheDocument();
expect(
within(result.getByTestId('sentry-config-editor-url')).queryByDisplayValue(DEFAULT_SENTRY_URL)
).not.toBeInTheDocument();
expect(
within(result.getByTestId('sentry-config-editor-url')).getByDisplayValue('https://foo.com')
).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-org-slug')).toBeInTheDocument();
expect(
within(result.getByTestId('sentry-config-editor-org-slug')).getByDisplayValue('my-org-slug')
).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-auth-token')).toBeInTheDocument();
expect(
within(result.getByTestId('sentry-config-editor-org-slug-row')).getByTestId('sentry-config-editor-org-slug')
within(result.getByTestId('sentry-config-editor-auth-token')).getByDisplayValue('Configured')
).toBeInTheDocument();
expect(within(result.getByTestId('sentry-config-editor-org-slug-row')).getByDisplayValue('my-org-slug')).toBeInTheDocument();
expect(result.getByTestId('sentry-config-editor-auth-token-row')).toBeInTheDocument();
expect(within(result.getByTestId('sentry-config-editor-auth-token-row')).getByDisplayValue('Configured')).toBeInTheDocument();
expect(
within(result.getByTestId('sentry-config-editor-auth-token-row')).getByText(
within(result.getByTestId('sentry-config-editor-auth-token')).getByText(
selectors.components.ConfigEditor.SentrySettings.AuthToken.Reset.label
)
).toBeInTheDocument();
expect(onOptionsChange).toBeCalledTimes(0);
userEvent.click(
within(result.getByTestId('sentry-config-editor-auth-token-row')).getByText(
within(result.getByTestId('sentry-config-editor-auth-token')).getByText(
selectors.components.ConfigEditor.SentrySettings.AuthToken.Reset.label
)
);
Expand Down
181 changes: 101 additions & 80 deletions src/editors/SentryConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import React, { useState } from 'react';
import { InlineFormLabel, Input, Button, Switch, useTheme } from '@grafana/ui';
import { Field, Input, Button, Switch } from '@grafana/ui';
import { Components } from './../selectors';
import { DEFAULT_SENTRY_URL } from './../constants';
import { config } from '@grafana/runtime';
import { gte } from 'semver';

import { config } from '@grafana/runtime';
import { DataSourceDescription, ConfigSection } from '@grafana/experimental';
import type { DataSourcePluginOptionsEditorProps } from '@grafana/data';

import type { SentryConfig, SentrySecureConfig } from './../types';
import { Divider } from 'components/Divider';
import { isVersionGtOrEq } from 'utils/version';

type SentryConfigEditorProps = {} & DataSourcePluginOptionsEditorProps<SentryConfig, SentrySecureConfig>;

export const SentryConfigEditor = (props: SentryConfigEditorProps) => {
const theme = useTheme();
const { options, onOptionsChange } = props;
const { jsonData, secureJsonFields } = options;
const secureJsonData: SentrySecureConfig = (options.secureJsonData || {}) as SentrySecureConfig;
const [url, setURL] = useState<string>(jsonData?.url || DEFAULT_SENTRY_URL);
const [orgSlug, setOrgSlug] = useState<string>(jsonData?.orgSlug || '');
const [authToken, setAuthToken] = useState<string>('');
const { ConfigEditor: ConfigEditorSelectors } = Components;
const labelWidth = 13;
const valueWidth = 20;
const onOptionChange = <Key extends keyof SentryConfig, Value extends SentryConfig[Key]>(option: Key, value: Value) => {
const onOptionChange = <Key extends keyof SentryConfig, Value extends SentryConfig[Key]>(
option: Key,
value: Value
) => {
onOptionsChange({
...options,
jsonData: { ...jsonData, [option]: value },
Expand All @@ -38,64 +42,80 @@ export const SentryConfigEditor = (props: SentryConfigEditorProps) => {
secureJsonFields: { ...secureJsonFields, [option]: set },
});
};
const switchContainerStyle: React.CSSProperties = {
padding: `0 ${theme.spacing.sm}`,
height: `${theme.spacing.formInputHeight}px`,
display: 'flex',
alignItems: 'center',
};

return (
<div className="grafana-sentry-datasource config-editor">
<h4 className="heading">{ConfigEditorSelectors.SentrySettings.GroupTitle}</h4>
<div className="gf-form" data-testid="sentry-config-editor-url-row">
<InlineFormLabel tooltip={ConfigEditorSelectors.SentrySettings.URL.tooltip} width={labelWidth}>
{ConfigEditorSelectors.SentrySettings.URL.label}
</InlineFormLabel>
<Input
<>
<DataSourceDescription
dataSourceName="Sentry"
docsLink="https://grafana.com/grafana/plugins/grafana-sentry-datasource/"
hasRequiredFields
/>
<Divider />
<ConfigSection title={ConfigEditorSelectors.SentrySettings.GroupTitle}>
<Field
required
label={ConfigEditorSelectors.SentrySettings.URL.label}
description={ConfigEditorSelectors.SentrySettings.URL.tooltip}
invalid={!jsonData.url}
error={'URL is required'}
data-testid="sentry-config-editor-url"
placeholder={ConfigEditorSelectors.SentrySettings.URL.placeholder}
aria-label={ConfigEditorSelectors.SentrySettings.URL.ariaLabel}
value={url}
onChange={(e) => setURL(e.currentTarget.value)}
onBlur={() => onOptionChange('url', url)}
width={valueWidth * 2}
></Input>
</div>
<div className="gf-form" data-testid="sentry-config-editor-org-slug-row">
<InlineFormLabel tooltip={ConfigEditorSelectors.SentrySettings.OrgSlug.tooltip} width={labelWidth}>
{ConfigEditorSelectors.SentrySettings.OrgSlug.label}
</InlineFormLabel>
<Input
>
<Input
placeholder={ConfigEditorSelectors.SentrySettings.URL.placeholder}
aria-label={ConfigEditorSelectors.SentrySettings.URL.ariaLabel}
value={url}
onChange={(e) => setURL(e.currentTarget.value)}
onBlur={() => onOptionChange('url', url)}
width={valueWidth * 2}
/>
</Field>
<Field
description={ConfigEditorSelectors.SentrySettings.OrgSlug.tooltip}
label={ConfigEditorSelectors.SentrySettings.OrgSlug.label}
required
invalid={!jsonData.orgSlug}
error={'Organisation is required'}
data-testid="sentry-config-editor-org-slug"
placeholder={ConfigEditorSelectors.SentrySettings.OrgSlug.placeholder}
aria-label={ConfigEditorSelectors.SentrySettings.OrgSlug.ariaLabel}
value={orgSlug}
onChange={(e) => setOrgSlug(e.currentTarget.value)}
onBlur={() => onOptionChange('orgSlug', orgSlug)}
width={valueWidth * 2}
></Input>
</div>
<div className="gf-form" data-testid="sentry-config-editor-auth-token-row">
<InlineFormLabel tooltip={ConfigEditorSelectors.SentrySettings.AuthToken.tooltip} width={labelWidth}>
{ConfigEditorSelectors.SentrySettings.AuthToken.label}
</InlineFormLabel>
>
<Input
placeholder={ConfigEditorSelectors.SentrySettings.OrgSlug.placeholder}
aria-label={ConfigEditorSelectors.SentrySettings.OrgSlug.ariaLabel}
value={orgSlug}
onChange={(e) => setOrgSlug(e.currentTarget.value)}
onBlur={() => onOptionChange('orgSlug', orgSlug)}
width={valueWidth * 2}
></Input>
</Field>
{secureJsonFields?.authToken ? (
<>
<Input type="text" value="Configured" disabled={true} width={valueWidth * 2}></Input>
<Button
variant="secondary"
className="reset-button"
onClick={() => {
setAuthToken('');
onSecureOptionChange('authToken', authToken, false);
}}
>
{ConfigEditorSelectors.SentrySettings.AuthToken.Reset.label}
</Button>
</>
<Field
label={ConfigEditorSelectors.SentrySettings.AuthToken.label}
description={ConfigEditorSelectors.SentrySettings.AuthToken.tooltip}
required
data-testid="sentry-config-editor-auth-token"
>
<div className="width-30" style={{ display: 'flex', gap: '4px' }}>
<Input type="text" value="Configured" disabled={true} width={valueWidth * 2}></Input>
<Button
variant="secondary"
className="reset-button"
onClick={() => {
setAuthToken('');
onSecureOptionChange('authToken', authToken, false);
}}
>
{ConfigEditorSelectors.SentrySettings.AuthToken.Reset.label}
</Button>
</div>
</Field>
) : (
<>
<Field
label={ConfigEditorSelectors.SentrySettings.AuthToken.label}
description={ConfigEditorSelectors.SentrySettings.AuthToken.tooltip}
required
invalid={!secureJsonData.authToken || !secureJsonFields?.authToken}
error={'Auth token is required'}
data-testid="sentry-config-editor-auth-token"
>
<Input
type="password"
autoComplete="new-password"
Expand All @@ -110,28 +130,29 @@ export const SentryConfigEditor = (props: SentryConfigEditorProps) => {
}
}}
></Input>
</>
</Field>
)}
</div>
<br />
{config.featureToggles['secureSocksDSProxyEnabled'] && gte(config.buildInfo.version, '10.0.0') && (
<div className="gf-form-group">
<h4>Additional Properties</h4>
<div className="gf-form">
<InlineFormLabel width={labelWidth} tooltip={Components.ConfigEditor.SecureSocksProxy.tooltip}>
{Components.ConfigEditor.SecureSocksProxy.label}
</InlineFormLabel>
<div style={switchContainerStyle}>
<Switch
className="gf-form"
value={jsonData.enableSecureSocksProxy || false}
onChange={(e) => onOptionChange('enableSecureSocksProxy', e.currentTarget.checked)}
/>
</div>
</div>
</div>
)}

</div>
</ConfigSection>
<Divider />
{config.featureToggles['secureSocksDSProxyEnabled'] && isVersionGtOrEq(config.buildInfo.version, '10.0.0') && (
<ConfigSection
title="Additional settings"
description="Additional settings are optional settings that can be configured for more control over your data source. This includes enabling the secure socks proxy."
isCollapsible
isInitiallyOpen={jsonData.enableSecureSocksProxy}
>
<Field
label={Components.ConfigEditor.SecureSocksProxy.label}
description={Components.ConfigEditor.SecureSocksProxy.tooltip}
>
<Switch
className="gf-form"
value={jsonData.enableSecureSocksProxy || false}
onChange={(e) => onOptionChange('enableSecureSocksProxy', e.currentTarget.checked)}
/>
</Field>
</ConfigSection>
)}
</>
);
};
54 changes: 54 additions & 0 deletions src/utils/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { SemVersion, isVersionGtOrEq } from './version'

describe('SemVersion', () => {
let version = '1.0.0-alpha.1'

describe('parsing', () => {
it('should parse version properly', () => {
const semver = new SemVersion(version)
expect(semver.major).toBe(1)
expect(semver.minor).toBe(0)
expect(semver.patch).toBe(0)
expect(semver.meta).toBe('alpha.1')
})
})

describe('comparing', () => {
beforeEach(() => {
version = '3.4.5'
})

it('should detect greater version properly', () => {
const semver = new SemVersion(version)
const cases = [
{ value: '3.4.5', expected: true },
{ value: '3.4.4', expected: true },
{ value: '3.4.6', expected: false },
{ value: '4', expected: false },
{ value: '3.5', expected: false },
]
cases.forEach((testCase) => {
expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected)
})
})
})

describe('isVersionGtOrEq', () => {
it('should compare versions properly (a >= b)', () => {
const cases = [
{ values: ['3.4.5', '3.4.5'], expected: true },
{ values: ['3.4.5', '3.4.4'], expected: true },
{ values: ['3.4.5', '3.4.6'], expected: false },
{ values: ['3.4', '3.4.0'], expected: true },
{ values: ['3', '3.0.0'], expected: true },
{ values: ['3.1.1-beta1', '3.1'], expected: true },
{ values: ['3.4.5', '4'], expected: false },
{ values: ['3.4.5', '3.5'], expected: false },
{ values: ['6.0.0', '5.2.0'], expected: true },
]
cases.forEach((testCase) => {
expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected)
})
})
})
})
Loading

0 comments on commit 79519c0

Please sign in to comment.