Skip to content

Commit

Permalink
Issue #34: add DataLinks and DataLink webcomponent
Browse files Browse the repository at this point in the history
  • Loading branch information
idrissneumann committed Dec 8, 2023
1 parent b597b8c commit a9ab533
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 0 deletions.
25 changes: 25 additions & 0 deletions src/components/Divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { css } from '@emotion/css';
import React from 'react';

import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';

export const Divider = ({ hideLine = false }) => {
const styles = useStyles2(getStyles);

if (hideLine) {
return <hr className={styles.dividerHideLine} />;
}

return <hr className={styles.divider} />;
};

const getStyles = (theme: GrafanaTheme2) => ({
divider: css({
margin: theme.spacing(4, 0),
}),
dividerHideLine: css({
border: 'none',
margin: theme.spacing(3, 0),
}),
});
24 changes: 24 additions & 0 deletions src/configuration/ConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { DataSourceHttpSettings, Input, InlineField, FieldSet } from '@grafana/u
import { DataSourcePluginOptionsEditorProps, DataSourceSettings } from '@grafana/data';
import { QuickwitOptions } from 'quickwit';
import { coerceOptions } from './utils';
import { Divider } from 'components/Divider';
import { DataLinks } from './DataLinks';

interface Props extends DataSourcePluginOptionsEditorProps<QuickwitOptions> {}

Expand All @@ -27,6 +29,7 @@ export const ConfigEditor = (props: Props) => {
onChange={onOptionsChange}
/>
<QuickwitDetails value={options} onChange={onSettingsChange} />
<QuickwitDataLinks value={options} onChange={onOptionsChange} />
</>
);
};
Expand All @@ -35,6 +38,27 @@ type DetailsProps = {
value: DataSourceSettings<QuickwitOptions>;
onChange: (value: DataSourceSettings<QuickwitOptions>) => void;
};

export const QuickwitDataLinks = ({ value, onChange }: DetailsProps) => {
return (
<div className="gf-form-group">
<Divider hideLine />
<DataLinks
value={value.jsonData.dataLinks}
onChange={(newValue) => {
onChange({
...value,
jsonData: {
...value.jsonData,
dataLinks: newValue,
},
});
}}
/>
</div>
)
};

export const QuickwitDetails = ({ value, onChange }: DetailsProps) => {
return (
<>
Expand Down
176 changes: 176 additions & 0 deletions src/configuration/DataLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { css } from '@emotion/css';
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { usePrevious } from 'react-use';

import { DataSourceInstanceSettings, VariableSuggestion } from '@grafana/data';
import {
Button,
DataLinkInput,
InlineField,
InlineSwitch,
InlineFieldRow,
InlineLabel,
Input,
useStyles2
} from '@grafana/ui';

import { DataSourcePicker } from '@grafana/runtime'

import { DataLinkConfig } from '../types';

interface Props {
value: DataLinkConfig;
onChange: (value: DataLinkConfig) => void;
onDelete: () => void;
suggestions: VariableSuggestion[];
className?: string;
}

export const DataLink = (props: Props) => {
const { value, onChange, onDelete, suggestions, className } = props;
const styles = useStyles2(getStyles);
const [showInternalLink, setShowInternalLink] = useInternalLink(value.datasourceUid);

const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
[field]: event.currentTarget.value,
});
};

return (
<div className={className}>
<div className={styles.firstRow}>
<InlineField
label="Field"
htmlFor="elasticsearch-datasource-config-field"
labelWidth={12}
tooltip={'Can be exact field name or a regex pattern that will match on the field name.'}
>
<Input
type="text"
id="elasticsearch-datasource-config-field"
value={value.field}
onChange={handleChange('field')}
width={100}
/>
</InlineField>
<Button
variant={'destructive'}
title="Remove field"
icon="times"
onClick={(event) => {
event.preventDefault();
onDelete();
}}
/>
</div>

<InlineFieldRow>
<div className={styles.urlField}>
<InlineLabel htmlFor="elasticsearch-datasource-internal-link" width={12}>
{showInternalLink ? 'Query' : 'URL'}
</InlineLabel>
<DataLinkInput
placeholder={showInternalLink ? '${__value.raw}' : 'http://example.com/${__value.raw}'}
value={value.url || ''}
onChange={(newValue) =>
onChange({
...value,
url: newValue,
})
}
suggestions={suggestions}
/>
</div>

<div className={styles.urlDisplayLabelField}>
<InlineField
label="URL Label"
htmlFor="elasticsearch-datasource-url-label"
labelWidth={14}
tooltip={'Use to override the button label.'}
>
<Input
type="text"
id="elasticsearch-datasource-url-label"
value={value.urlDisplayLabel}
onChange={handleChange('urlDisplayLabel')}
/>
</InlineField>
</div>
</InlineFieldRow>

<div className={styles.row}>
<InlineField label="Internal link" labelWidth={12}>
<InlineSwitch
label="Internal link"
value={showInternalLink || false}
onChange={() => {
if (showInternalLink) {
onChange({
...value,
datasourceUid: undefined,
});
}
setShowInternalLink(!showInternalLink);
}}
/>
</InlineField>

{showInternalLink && (
<DataSourcePicker
tracing={true}
onChange={(ds: DataSourceInstanceSettings) => {
onChange({
...value,
datasourceUid: ds.uid,
});
}}
current={value.datasourceUid}
/>
)}
</div>
</div>
);
};

function useInternalLink(datasourceUid?: string): [boolean, Dispatch<SetStateAction<boolean>>] {
const [showInternalLink, setShowInternalLink] = useState<boolean>(!!datasourceUid);
const previousUid = usePrevious(datasourceUid);

// Force internal link visibility change if uid changed outside of this component.
useEffect(() => {
if (!previousUid && datasourceUid && !showInternalLink) {
setShowInternalLink(true);
}
if (previousUid && !datasourceUid && showInternalLink) {
setShowInternalLink(false);
}
}, [previousUid, datasourceUid, showInternalLink]);

return [showInternalLink, setShowInternalLink];
}

const getStyles = () => ({
firstRow: css`
display: flex;
`,
nameField: css`
flex: 2;
`,
regexField: css`
flex: 3;
`,
row: css`
display: flex;
align-items: baseline;
`,
urlField: css`
display: flex;
flex: 1;
`,
urlDisplayLabelField: css`
flex: 1;
`,
});
67 changes: 67 additions & 0 deletions src/configuration/DataLinks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { DataLinkConfig } from '../types';

import { DataLinks, Props } from './DataLinks';

const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
value: [],
onChange: jest.fn(),
...propOverrides,
};

return render(<DataLinks {...props} />);
};

describe('DataLinks tests', () => {
it('should render correctly with no fields', async () => {
setup();

expect(screen.getByRole('heading', { name: 'Data links' }));
expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument();
expect(await screen.findAllByRole('button')).toHaveLength(1);
});

it('should render correctly when passed fields', async () => {
setup({ value: testValue });

expect(await screen.findAllByRole('button', { name: 'Remove field' })).toHaveLength(2);
expect(await screen.findAllByRole('checkbox', { name: 'Internal link' })).toHaveLength(2);
});

it('should call onChange to add a new field when the add button is clicked', async () => {
const onChangeMock = jest.fn();
setup({ onChange: onChangeMock });

expect(onChangeMock).not.toHaveBeenCalled();
const addButton = screen.getByRole('button', { name: 'Add' });
await userEvent.click(addButton);

expect(onChangeMock).toHaveBeenCalled();
});

it('should call onChange to remove a field when the remove button is clicked', async () => {
const onChangeMock = jest.fn();
setup({ value: testValue, onChange: onChangeMock });

expect(onChangeMock).not.toHaveBeenCalled();
const removeButton = await screen.findAllByRole('button', { name: 'Remove field' });
await userEvent.click(removeButton[0]);

expect(onChangeMock).toHaveBeenCalled();
});
});

const testValue: DataLinkConfig[] = [
{
field: 'regex1',
url: 'localhost1',
},
{
field: 'regex2',
url: 'localhost2',
},
];
Loading

0 comments on commit a9ab533

Please sign in to comment.