Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #34: datalinks / correlation #35

Merged
merged 9 commits into from
Dec 13, 2023
18 changes: 18 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
version: '3.0'

services:
jaeger:
image: jaegertracing/jaeger-query:1.51
container_name: 'jaeger-quickwit'
environment:
- GRPC_STORAGE_SERVER=host.docker.internal:7281
- SPAN_STORAGE_TYPE=grpc-plugin
ports:
- 16686:16686
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- quickwit
grafana:
container_name: 'grafana-quickwit-datasource'
build:
Expand All @@ -15,6 +27,12 @@ services:
- gquickwit:/var/lib/grafana
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- quickwit

networks:
quickwit:
driver: bridge

volumes:
gquickwit:
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
192 changes: 192 additions & 0 deletions src/configuration/DataLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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 [base64TraceId, setBase64TraceId] = useState(true)
const labelWidth = 24

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

const handleBase64TraceId = (base64TraceId: boolean, config: DataLinkConfig) => {
setBase64TraceId(base64TraceId)
config = {...config, base64TraceId: base64TraceId };
}

return (
<div className={className}>
<div className={styles.firstRow}>
<InlineField
label="Field"
htmlFor="elasticsearch-datasource-config-field"
labelWidth={labelWidth}
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={labelWidth}>
{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="Field encoded in base64?" labelWidth={labelWidth} tooltip="Quickwit encodes the traceID in base64 by default whereas Jaeger uses hex">
<InlineSwitch
value={base64TraceId}
onChange={() => handleBase64TraceId(!base64TraceId, value)}
/>
</InlineField>
</div>

<div className={styles.row}>
<InlineField label="Internal link" labelWidth={labelWidth}>
<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;
`,
});
69 changes: 69 additions & 0 deletions src/configuration/DataLinks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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',
base64TraceId: false,
},
{
field: 'regex2',
url: 'localhost2',
base64TraceId: true,
},
];
Loading