Skip to content

Commit d9305e2

Browse files
committed
Issue #34: add DataLinks and DataLink webcomponent
1 parent b70d883 commit d9305e2

File tree

5 files changed

+380
-0
lines changed

5 files changed

+380
-0
lines changed

src/components/Divider.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { css } from '@emotion/css';
2+
import React from 'react';
3+
4+
import { GrafanaTheme2 } from '@grafana/data';
5+
import { useStyles2 } from '@grafana/ui';
6+
7+
export const Divider = ({ hideLine = false }) => {
8+
const styles = useStyles2(getStyles);
9+
10+
if (hideLine) {
11+
return <hr className={styles.dividerHideLine} />;
12+
}
13+
14+
return <hr className={styles.divider} />;
15+
};
16+
17+
const getStyles = (theme: GrafanaTheme2) => ({
18+
divider: css({
19+
margin: theme.spacing(4, 0),
20+
}),
21+
dividerHideLine: css({
22+
border: 'none',
23+
margin: theme.spacing(3, 0),
24+
}),
25+
});

src/configuration/ConfigEditor.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { DataSourceHttpSettings, Input, InlineField, FieldSet } from '@grafana/u
33
import { DataSourcePluginOptionsEditorProps, DataSourceSettings } from '@grafana/data';
44
import { QuickwitOptions } from 'quickwit';
55
import { coerceOptions } from './utils';
6+
import { Divider } from 'components/Divider';
7+
import { DataLinks } from './DataLinks';
68

79
interface Props extends DataSourcePluginOptionsEditorProps<QuickwitOptions> {}
810

@@ -27,6 +29,7 @@ export const ConfigEditor = (props: Props) => {
2729
onChange={onOptionsChange}
2830
/>
2931
<QuickwitDetails value={options} onChange={onSettingsChange} />
32+
<QuickwitDataLinks value={options} onChange={onOptionsChange} />
3033
</>
3134
);
3235
};
@@ -35,6 +38,27 @@ type DetailsProps = {
3538
value: DataSourceSettings<QuickwitOptions>;
3639
onChange: (value: DataSourceSettings<QuickwitOptions>) => void;
3740
};
41+
42+
export const QuickwitDataLinks = ({ value, onChange }: DetailsProps) => {
43+
return (
44+
<div className="gf-form-group">
45+
<Divider hideLine />
46+
<DataLinks
47+
value={value.jsonData.dataLinks}
48+
onChange={(newValue) => {
49+
onChange({
50+
...value,
51+
jsonData: {
52+
...value.jsonData,
53+
dataLinks: newValue,
54+
},
55+
});
56+
}}
57+
/>
58+
</div>
59+
)
60+
};
61+
3862
export const QuickwitDetails = ({ value, onChange }: DetailsProps) => {
3963
return (
4064
<>

src/configuration/DataLink.tsx

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { css } from '@emotion/css';
2+
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
3+
import { usePrevious } from 'react-use';
4+
5+
import { DataSourceInstanceSettings, VariableSuggestion } from '@grafana/data';
6+
import {
7+
Button,
8+
DataLinkInput,
9+
InlineField,
10+
InlineSwitch,
11+
InlineFieldRow,
12+
InlineLabel,
13+
Input,
14+
useStyles2
15+
} from '@grafana/ui';
16+
17+
import { DataSourcePicker } from '@grafana/runtime'
18+
19+
import { DataLinkConfig } from '../types';
20+
21+
interface Props {
22+
value: DataLinkConfig;
23+
onChange: (value: DataLinkConfig) => void;
24+
onDelete: () => void;
25+
suggestions: VariableSuggestion[];
26+
className?: string;
27+
}
28+
29+
export const DataLink = (props: Props) => {
30+
const { value, onChange, onDelete, suggestions, className } = props;
31+
const styles = useStyles2(getStyles);
32+
const [showInternalLink, setShowInternalLink] = useInternalLink(value.datasourceUid);
33+
34+
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
35+
onChange({
36+
...value,
37+
[field]: event.currentTarget.value,
38+
});
39+
};
40+
41+
return (
42+
<div className={className}>
43+
<div className={styles.firstRow}>
44+
<InlineField
45+
label="Field"
46+
htmlFor="elasticsearch-datasource-config-field"
47+
labelWidth={12}
48+
tooltip={'Can be exact field name or a regex pattern that will match on the field name.'}
49+
>
50+
<Input
51+
type="text"
52+
id="elasticsearch-datasource-config-field"
53+
value={value.field}
54+
onChange={handleChange('field')}
55+
width={100}
56+
/>
57+
</InlineField>
58+
<Button
59+
variant={'destructive'}
60+
title="Remove field"
61+
icon="times"
62+
onClick={(event) => {
63+
event.preventDefault();
64+
onDelete();
65+
}}
66+
/>
67+
</div>
68+
69+
<InlineFieldRow>
70+
<div className={styles.urlField}>
71+
<InlineLabel htmlFor="elasticsearch-datasource-internal-link" width={12}>
72+
{showInternalLink ? 'Query' : 'URL'}
73+
</InlineLabel>
74+
<DataLinkInput
75+
placeholder={showInternalLink ? '${__value.raw}' : 'http://example.com/${__value.raw}'}
76+
value={value.url || ''}
77+
onChange={(newValue) =>
78+
onChange({
79+
...value,
80+
url: newValue,
81+
})
82+
}
83+
suggestions={suggestions}
84+
/>
85+
</div>
86+
87+
<div className={styles.urlDisplayLabelField}>
88+
<InlineField
89+
label="URL Label"
90+
htmlFor="elasticsearch-datasource-url-label"
91+
labelWidth={14}
92+
tooltip={'Use to override the button label.'}
93+
>
94+
<Input
95+
type="text"
96+
id="elasticsearch-datasource-url-label"
97+
value={value.urlDisplayLabel}
98+
onChange={handleChange('urlDisplayLabel')}
99+
/>
100+
</InlineField>
101+
</div>
102+
</InlineFieldRow>
103+
104+
<div className={styles.row}>
105+
<InlineField label="Internal link" labelWidth={12}>
106+
<InlineSwitch
107+
label="Internal link"
108+
value={showInternalLink || false}
109+
onChange={() => {
110+
if (showInternalLink) {
111+
onChange({
112+
...value,
113+
datasourceUid: undefined,
114+
});
115+
}
116+
setShowInternalLink(!showInternalLink);
117+
}}
118+
/>
119+
</InlineField>
120+
121+
{showInternalLink && (
122+
<DataSourcePicker
123+
tracing={true}
124+
onChange={(ds: DataSourceInstanceSettings) => {
125+
onChange({
126+
...value,
127+
datasourceUid: ds.uid,
128+
});
129+
}}
130+
current={value.datasourceUid}
131+
/>
132+
)}
133+
</div>
134+
</div>
135+
);
136+
};
137+
138+
function useInternalLink(datasourceUid?: string): [boolean, Dispatch<SetStateAction<boolean>>] {
139+
const [showInternalLink, setShowInternalLink] = useState<boolean>(!!datasourceUid);
140+
const previousUid = usePrevious(datasourceUid);
141+
142+
// Force internal link visibility change if uid changed outside of this component.
143+
useEffect(() => {
144+
if (!previousUid && datasourceUid && !showInternalLink) {
145+
setShowInternalLink(true);
146+
}
147+
if (previousUid && !datasourceUid && showInternalLink) {
148+
setShowInternalLink(false);
149+
}
150+
}, [previousUid, datasourceUid, showInternalLink]);
151+
152+
return [showInternalLink, setShowInternalLink];
153+
}
154+
155+
const getStyles = () => ({
156+
firstRow: css`
157+
display: flex;
158+
`,
159+
nameField: css`
160+
flex: 2;
161+
`,
162+
regexField: css`
163+
flex: 3;
164+
`,
165+
row: css`
166+
display: flex;
167+
align-items: baseline;
168+
`,
169+
urlField: css`
170+
display: flex;
171+
flex: 1;
172+
`,
173+
urlDisplayLabelField: css`
174+
flex: 1;
175+
`,
176+
});

src/configuration/DataLinks.test.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import React from 'react';
4+
5+
import { DataLinkConfig } from '../types';
6+
7+
import { DataLinks, Props } from './DataLinks';
8+
9+
const setup = (propOverrides?: Partial<Props>) => {
10+
const props: Props = {
11+
value: [],
12+
onChange: jest.fn(),
13+
...propOverrides,
14+
};
15+
16+
return render(<DataLinks {...props} />);
17+
};
18+
19+
describe('DataLinks tests', () => {
20+
it('should render correctly with no fields', async () => {
21+
setup();
22+
23+
expect(screen.getByRole('heading', { name: 'Data links' }));
24+
expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument();
25+
expect(await screen.findAllByRole('button')).toHaveLength(1);
26+
});
27+
28+
it('should render correctly when passed fields', async () => {
29+
setup({ value: testValue });
30+
31+
expect(await screen.findAllByRole('button', { name: 'Remove field' })).toHaveLength(2);
32+
expect(await screen.findAllByRole('checkbox', { name: 'Internal link' })).toHaveLength(2);
33+
});
34+
35+
it('should call onChange to add a new field when the add button is clicked', async () => {
36+
const onChangeMock = jest.fn();
37+
setup({ onChange: onChangeMock });
38+
39+
expect(onChangeMock).not.toHaveBeenCalled();
40+
const addButton = screen.getByRole('button', { name: 'Add' });
41+
await userEvent.click(addButton);
42+
43+
expect(onChangeMock).toHaveBeenCalled();
44+
});
45+
46+
it('should call onChange to remove a field when the remove button is clicked', async () => {
47+
const onChangeMock = jest.fn();
48+
setup({ value: testValue, onChange: onChangeMock });
49+
50+
expect(onChangeMock).not.toHaveBeenCalled();
51+
const removeButton = await screen.findAllByRole('button', { name: 'Remove field' });
52+
await userEvent.click(removeButton[0]);
53+
54+
expect(onChangeMock).toHaveBeenCalled();
55+
});
56+
});
57+
58+
const testValue: DataLinkConfig[] = [
59+
{
60+
field: 'regex1',
61+
url: 'localhost1',
62+
},
63+
{
64+
field: 'regex2',
65+
url: 'localhost2',
66+
},
67+
];

0 commit comments

Comments
 (0)