Skip to content

Commit 9bea584

Browse files
authored
Merge pull request #35 from quickwit-oss/feat_datalinks
Issue #34: datalinks / correlation
2 parents 28ebe43 + 65ff78a commit 9bea584

File tree

8 files changed

+516
-15
lines changed

8 files changed

+516
-15
lines changed

docker-compose.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
version: '3.0'
22

33
services:
4+
jaeger:
5+
image: jaegertracing/jaeger-query:1.51
6+
container_name: 'jaeger-quickwit'
7+
environment:
8+
- GRPC_STORAGE_SERVER=host.docker.internal:7281
9+
- SPAN_STORAGE_TYPE=grpc-plugin
10+
ports:
11+
- 16686:16686
12+
extra_hosts:
13+
- "host.docker.internal:host-gateway"
14+
networks:
15+
- quickwit
416
grafana:
517
container_name: 'grafana-quickwit-datasource'
618
build:
@@ -15,6 +27,12 @@ services:
1527
- gquickwit:/var/lib/grafana
1628
extra_hosts:
1729
- "host.docker.internal:host-gateway"
30+
networks:
31+
- quickwit
32+
33+
networks:
34+
quickwit:
35+
driver: bridge
1836

1937
volumes:
2038
gquickwit:

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: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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+
const [base64TraceId, setBase64TraceId] = useState(true)
34+
const labelWidth = 24
35+
36+
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
37+
onChange({
38+
...value,
39+
[field]: event.currentTarget.value,
40+
});
41+
};
42+
43+
const handleBase64TraceId = (base64TraceId: boolean, config: DataLinkConfig) => {
44+
setBase64TraceId(base64TraceId)
45+
config = {...config, base64TraceId: base64TraceId };
46+
}
47+
48+
return (
49+
<div className={className}>
50+
<div className={styles.firstRow}>
51+
<InlineField
52+
label="Field"
53+
htmlFor="elasticsearch-datasource-config-field"
54+
labelWidth={labelWidth}
55+
tooltip={'Can be exact field name or a regex pattern that will match on the field name.'}
56+
>
57+
<Input
58+
type="text"
59+
id="elasticsearch-datasource-config-field"
60+
value={value.field}
61+
onChange={handleChange('field')}
62+
width={100}
63+
/>
64+
</InlineField>
65+
<Button
66+
variant={'destructive'}
67+
title="Remove field"
68+
icon="times"
69+
onClick={(event) => {
70+
event.preventDefault();
71+
onDelete();
72+
}}
73+
/>
74+
</div>
75+
76+
<InlineFieldRow>
77+
<div className={styles.urlField}>
78+
<InlineLabel htmlFor="elasticsearch-datasource-internal-link" width={labelWidth}>
79+
{showInternalLink ? 'Query' : 'URL'}
80+
</InlineLabel>
81+
<DataLinkInput
82+
placeholder={showInternalLink ? '${__value.raw}' : 'http://example.com/${__value.raw}'}
83+
value={value.url || ''}
84+
onChange={(newValue) =>
85+
onChange({
86+
...value,
87+
url: newValue,
88+
})
89+
}
90+
suggestions={suggestions}
91+
/>
92+
</div>
93+
94+
<div className={styles.urlDisplayLabelField}>
95+
<InlineField
96+
label="URL Label"
97+
htmlFor="elasticsearch-datasource-url-label"
98+
labelWidth={14}
99+
tooltip={'Use to override the button label.'}
100+
>
101+
<Input
102+
type="text"
103+
id="elasticsearch-datasource-url-label"
104+
value={value.urlDisplayLabel}
105+
onChange={handleChange('urlDisplayLabel')}
106+
/>
107+
</InlineField>
108+
</div>
109+
</InlineFieldRow>
110+
111+
<div className={styles.row}>
112+
<InlineField label="Field encoded in base64?" labelWidth={labelWidth} tooltip="Quickwit encodes the traceID in base64 by default whereas Jaeger uses hex">
113+
<InlineSwitch
114+
value={base64TraceId}
115+
onChange={() => handleBase64TraceId(!base64TraceId, value)}
116+
/>
117+
</InlineField>
118+
</div>
119+
120+
<div className={styles.row}>
121+
<InlineField label="Internal link" labelWidth={labelWidth}>
122+
<InlineSwitch
123+
label="Internal link"
124+
value={showInternalLink || false}
125+
onChange={() => {
126+
if (showInternalLink) {
127+
onChange({
128+
...value,
129+
datasourceUid: undefined,
130+
});
131+
}
132+
setShowInternalLink(!showInternalLink);
133+
}}
134+
/>
135+
</InlineField>
136+
137+
{showInternalLink && (
138+
<DataSourcePicker
139+
tracing={true}
140+
onChange={(ds: DataSourceInstanceSettings) => {
141+
onChange({
142+
...value,
143+
datasourceUid: ds.uid,
144+
});
145+
}}
146+
current={value.datasourceUid}
147+
/>
148+
)}
149+
</div>
150+
</div>
151+
);
152+
};
153+
154+
function useInternalLink(datasourceUid?: string): [boolean, Dispatch<SetStateAction<boolean>>] {
155+
const [showInternalLink, setShowInternalLink] = useState<boolean>(!!datasourceUid);
156+
const previousUid = usePrevious(datasourceUid);
157+
158+
// Force internal link visibility change if uid changed outside of this component.
159+
useEffect(() => {
160+
if (!previousUid && datasourceUid && !showInternalLink) {
161+
setShowInternalLink(true);
162+
}
163+
if (previousUid && !datasourceUid && showInternalLink) {
164+
setShowInternalLink(false);
165+
}
166+
}, [previousUid, datasourceUid, showInternalLink]);
167+
168+
return [showInternalLink, setShowInternalLink];
169+
}
170+
171+
const getStyles = () => ({
172+
firstRow: css`
173+
display: flex;
174+
`,
175+
nameField: css`
176+
flex: 2;
177+
`,
178+
regexField: css`
179+
flex: 3;
180+
`,
181+
row: css`
182+
display: flex;
183+
align-items: baseline;
184+
`,
185+
urlField: css`
186+
display: flex;
187+
flex: 1;
188+
`,
189+
urlDisplayLabelField: css`
190+
flex: 1;
191+
`,
192+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
base64TraceId: false,
63+
},
64+
{
65+
field: 'regex2',
66+
url: 'localhost2',
67+
base64TraceId: true,
68+
},
69+
];

0 commit comments

Comments
 (0)