Skip to content

Commit dc8a5ca

Browse files
committed
Issue #34: add missing webcomponents
1 parent b597b8c commit dc8a5ca

File tree

3 files changed

+331
-0
lines changed

3 files changed

+331
-0
lines changed

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+
];

src/configuration/DataLinks.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { css } from '@emotion/css';
2+
import React from 'react';
3+
4+
import { GrafanaTheme2, VariableOrigin, DataLinkBuiltInVars } from '@grafana/data';
5+
import { ConfigSubSection } from '@grafana/experimental';
6+
import { Button, useStyles2 } from '@grafana/ui';
7+
8+
import { DataLinkConfig } from '../types';
9+
10+
import { DataLink } from './DataLink';
11+
12+
const getStyles = (theme: GrafanaTheme2) => {
13+
return {
14+
addButton: css`
15+
margin-right: 10px;
16+
`,
17+
container: css`
18+
margin-bottom: ${theme.spacing(2)};
19+
`,
20+
dataLink: css`
21+
margin-bottom: ${theme.spacing(1)};
22+
`,
23+
};
24+
};
25+
26+
export type Props = {
27+
value?: DataLinkConfig[];
28+
onChange: (value: DataLinkConfig[]) => void;
29+
};
30+
export const DataLinks = (props: Props) => {
31+
const { value, onChange } = props;
32+
const styles = useStyles2(getStyles);
33+
34+
return (
35+
<ConfigSubSection
36+
title="Data links"
37+
description="Add links to existing fields. Links will be shown in log row details next to the field value."
38+
>
39+
<div className={styles.container}>
40+
{value && value.length > 0 && (
41+
<div className="gf-form-group">
42+
{value.map((field, index) => {
43+
return (
44+
<DataLink
45+
className={styles.dataLink}
46+
key={index}
47+
value={field}
48+
onChange={(newField) => {
49+
const newDataLinks = [...value];
50+
newDataLinks.splice(index, 1, newField);
51+
onChange(newDataLinks);
52+
}}
53+
onDelete={() => {
54+
const newDataLinks = [...value];
55+
newDataLinks.splice(index, 1);
56+
onChange(newDataLinks);
57+
}}
58+
suggestions={[
59+
{
60+
value: DataLinkBuiltInVars.valueRaw,
61+
label: 'Raw value',
62+
documentation: 'Raw value of the field',
63+
origin: VariableOrigin.Value,
64+
},
65+
]}
66+
/>
67+
);
68+
})}
69+
</div>
70+
)}
71+
72+
<Button
73+
type="button"
74+
variant={'secondary'}
75+
className={styles.addButton}
76+
icon="plus"
77+
onClick={(event) => {
78+
event.preventDefault();
79+
const newDataLinks = [...(value || []), { field: '', url: '' }];
80+
onChange(newDataLinks);
81+
}}
82+
>
83+
Add
84+
</Button>
85+
</div>
86+
</ConfigSubSection>
87+
);
88+
};

0 commit comments

Comments
 (0)