-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Issue #34: add DataLinks and DataLink webcomponent
- Loading branch information
1 parent
b597b8c
commit 14d6a88
Showing
3 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
`, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { css } from '@emotion/css'; | ||
import React from 'react'; | ||
|
||
import { GrafanaTheme2, VariableOrigin, DataLinkBuiltInVars } from '@grafana/data'; | ||
import { ConfigSubSection } from '@grafana/experimental'; | ||
import { Button, useStyles2 } from '@grafana/ui'; | ||
|
||
import { DataLinkConfig } from '../types'; | ||
|
||
import { DataLink } from './DataLink'; | ||
|
||
const getStyles = (theme: GrafanaTheme2) => { | ||
return { | ||
addButton: css` | ||
margin-right: 10px; | ||
`, | ||
container: css` | ||
margin-bottom: ${theme.spacing(2)}; | ||
`, | ||
dataLink: css` | ||
margin-bottom: ${theme.spacing(1)}; | ||
`, | ||
}; | ||
}; | ||
|
||
export type Props = { | ||
value?: DataLinkConfig[]; | ||
onChange: (value: DataLinkConfig[]) => void; | ||
}; | ||
export const DataLinks = (props: Props) => { | ||
const { value, onChange } = props; | ||
const styles = useStyles2(getStyles); | ||
|
||
return ( | ||
<ConfigSubSection | ||
title="Data links" | ||
description="Add links to existing fields. Links will be shown in log row details next to the field value." | ||
> | ||
<div className={styles.container}> | ||
{value && value.length > 0 && ( | ||
<div className="gf-form-group"> | ||
{value.map((field, index) => { | ||
return ( | ||
<DataLink | ||
className={styles.dataLink} | ||
key={index} | ||
value={field} | ||
onChange={(newField) => { | ||
const newDataLinks = [...value]; | ||
newDataLinks.splice(index, 1, newField); | ||
onChange(newDataLinks); | ||
}} | ||
onDelete={() => { | ||
const newDataLinks = [...value]; | ||
newDataLinks.splice(index, 1); | ||
onChange(newDataLinks); | ||
}} | ||
suggestions={[ | ||
{ | ||
value: DataLinkBuiltInVars.valueRaw, | ||
label: 'Raw value', | ||
documentation: 'Raw value of the field', | ||
origin: VariableOrigin.Value, | ||
}, | ||
]} | ||
/> | ||
); | ||
})} | ||
</div> | ||
)} | ||
|
||
<Button | ||
type="button" | ||
variant={'secondary'} | ||
className={styles.addButton} | ||
icon="plus" | ||
onClick={(event) => { | ||
event.preventDefault(); | ||
const newDataLinks = [...(value || []), { field: '', url: '' }]; | ||
onChange(newDataLinks); | ||
}} | ||
> | ||
Add | ||
</Button> | ||
</div> | ||
</ConfigSubSection> | ||
); | ||
}; |