|
| 1 | +import { css } from '@emotion/css'; |
| 2 | +import { ChangeEvent, useEffect, useState } from 'react'; |
| 3 | +import * as React from 'react'; |
| 4 | +import { usePrevious } from 'react-use'; |
| 5 | + |
| 6 | +import { GrafanaTheme2, DataSourceInstanceSettings, VariableSuggestion } from '@grafana/data'; |
| 7 | +import { DataSourcePicker } from '@grafana/runtime'; |
| 8 | +import { Button, DataLinkInput, Field, Icon, Input, Label, Tooltip, useStyles2, Select, Switch } from '@grafana/ui'; |
| 9 | + |
| 10 | +import { DerivedFieldConfig } from '../types'; |
| 11 | + |
| 12 | +type MatcherType = 'label' | 'regex'; |
| 13 | + |
| 14 | +const getStyles = (theme: GrafanaTheme2) => ({ |
| 15 | + row: css({ |
| 16 | + display: 'flex', |
| 17 | + alignItems: 'baseline', |
| 18 | + }), |
| 19 | + nameField: css({ |
| 20 | + flex: 2, |
| 21 | + marginRight: theme.spacing(0.5), |
| 22 | + }), |
| 23 | + regexField: css({ |
| 24 | + flex: 3, |
| 25 | + marginRight: theme.spacing(0.5), |
| 26 | + }), |
| 27 | + urlField: css({ |
| 28 | + flex: 1, |
| 29 | + marginRight: theme.spacing(0.5), |
| 30 | + }), |
| 31 | + urlDisplayLabelField: css({ |
| 32 | + flex: 1, |
| 33 | + }), |
| 34 | + internalLink: css({ |
| 35 | + marginRight: theme.spacing(1), |
| 36 | + }), |
| 37 | + openNewTab: css({ |
| 38 | + marginRight: theme.spacing(1), |
| 39 | + }), |
| 40 | + dataSource: css({}), |
| 41 | + nameMatcherField: css({ |
| 42 | + width: theme.spacing(20), |
| 43 | + marginRight: theme.spacing(0.5), |
| 44 | + }), |
| 45 | +}); |
| 46 | + |
| 47 | +type Props = { |
| 48 | + value: DerivedFieldConfig; |
| 49 | + onChange: (value: DerivedFieldConfig) => void; |
| 50 | + onDelete: () => void; |
| 51 | + suggestions: VariableSuggestion[]; |
| 52 | + className?: string; |
| 53 | + validateName: (name: string) => boolean; |
| 54 | +}; |
| 55 | +export const DerivedField = (props: Props) => { |
| 56 | + const { value, onChange, onDelete, suggestions, className, validateName } = props; |
| 57 | + const styles = useStyles2(getStyles); |
| 58 | + const [showInternalLink, setShowInternalLink] = useState(!!value.datasourceUid); |
| 59 | + const previousUid = usePrevious(value.datasourceUid); |
| 60 | + const [fieldType, setFieldType] = useState<MatcherType>(value.matcherType ?? 'regex'); |
| 61 | + |
| 62 | + // Force internal link visibility change if uid changed outside of this component. |
| 63 | + useEffect(() => { |
| 64 | + if (!previousUid && value.datasourceUid && !showInternalLink) { |
| 65 | + setShowInternalLink(true); |
| 66 | + } |
| 67 | + if (previousUid && !value.datasourceUid && showInternalLink) { |
| 68 | + setShowInternalLink(false); |
| 69 | + } |
| 70 | + }, [previousUid, value.datasourceUid, showInternalLink]); |
| 71 | + |
| 72 | + const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => { |
| 73 | + onChange({ |
| 74 | + ...value, |
| 75 | + [field]: event.currentTarget.value, |
| 76 | + }); |
| 77 | + }; |
| 78 | + |
| 79 | + const invalidName = !validateName(value.name); |
| 80 | + |
| 81 | + return ( |
| 82 | + <div className={className} data-testid="derived-field"> |
| 83 | + <div className="gf-form"> |
| 84 | + <Field className={styles.nameField} label="Name" invalid={invalidName} error="The name is already in use"> |
| 85 | + <Input value={value.name} onChange={handleChange('name')} placeholder="Field name" invalid={invalidName} /> |
| 86 | + </Field> |
| 87 | + <Field |
| 88 | + className={styles.nameMatcherField} |
| 89 | + label={ |
| 90 | + <TooltipLabel |
| 91 | + label="Type" |
| 92 | + content="Derived fields can be created from labels or by applying a regular expression to the log message." |
| 93 | + /> |
| 94 | + } |
| 95 | + > |
| 96 | + <Select |
| 97 | + options={[ |
| 98 | + { label: 'Regex in log line', value: 'regex' }, |
| 99 | + { label: 'Label', value: 'label' }, |
| 100 | + ]} |
| 101 | + value={fieldType} |
| 102 | + onChange={(type) => { |
| 103 | + // make sure this is a valid MatcherType |
| 104 | + if (type.value === 'label' || type.value === 'regex') { |
| 105 | + setFieldType(type.value); |
| 106 | + onChange({ |
| 107 | + ...value, |
| 108 | + matcherType: type.value, |
| 109 | + }); |
| 110 | + } |
| 111 | + }} |
| 112 | + /> |
| 113 | + </Field> |
| 114 | + <Field |
| 115 | + className={styles.regexField} |
| 116 | + label={ |
| 117 | + <> |
| 118 | + {fieldType === 'regex' && ( |
| 119 | + <TooltipLabel |
| 120 | + label="Regex" |
| 121 | + content="Use to parse and capture some part of the log message. You can use the captured groups in the template." |
| 122 | + /> |
| 123 | + )} |
| 124 | + |
| 125 | + {fieldType === 'label' && <TooltipLabel label="Label" content="Use to derive the field from a label." />} |
| 126 | + </> |
| 127 | + } |
| 128 | + > |
| 129 | + <Input value={value.matcherRegex} onChange={handleChange('matcherRegex')} /> |
| 130 | + </Field> |
| 131 | + <Field label=""> |
| 132 | + <Button |
| 133 | + variant="destructive" |
| 134 | + title="Remove field" |
| 135 | + icon="times" |
| 136 | + onClick={(event) => { |
| 137 | + event.preventDefault(); |
| 138 | + onDelete(); |
| 139 | + }} |
| 140 | + /> |
| 141 | + </Field> |
| 142 | + </div> |
| 143 | + |
| 144 | + <div className="gf-form"> |
| 145 | + <Field label={showInternalLink ? 'Query' : 'URL'} className={styles.urlField}> |
| 146 | + <DataLinkInput |
| 147 | + placeholder={showInternalLink ? '${__value.raw}' : 'http://example.com/${__value.raw}'} |
| 148 | + value={value.url || ''} |
| 149 | + onChange={(newValue) => |
| 150 | + onChange({ |
| 151 | + ...value, |
| 152 | + url: newValue, |
| 153 | + }) |
| 154 | + } |
| 155 | + suggestions={suggestions} |
| 156 | + /> |
| 157 | + </Field> |
| 158 | + <Field |
| 159 | + className={styles.urlDisplayLabelField} |
| 160 | + label={ |
| 161 | + <TooltipLabel |
| 162 | + label="URL Label" |
| 163 | + content="Use to override the button label when this derived field is found in a log." |
| 164 | + /> |
| 165 | + } |
| 166 | + > |
| 167 | + <Input value={value.urlDisplayLabel} onChange={handleChange('urlDisplayLabel')} /> |
| 168 | + </Field> |
| 169 | + </div> |
| 170 | + |
| 171 | + <div className="gf-form"> |
| 172 | + <Field label="Internal link" className={styles.internalLink}> |
| 173 | + <Switch |
| 174 | + value={showInternalLink} |
| 175 | + onChange={(e: ChangeEvent<HTMLInputElement>) => { |
| 176 | + const { checked } = e.currentTarget; |
| 177 | + if (!checked) { |
| 178 | + onChange({ |
| 179 | + ...value, |
| 180 | + datasourceUid: undefined, |
| 181 | + }); |
| 182 | + } |
| 183 | + setShowInternalLink(checked); |
| 184 | + }} |
| 185 | + /> |
| 186 | + </Field> |
| 187 | + |
| 188 | + {showInternalLink && ( |
| 189 | + <Field label="" className={styles.dataSource}> |
| 190 | + <DataSourcePicker |
| 191 | + tracing={true} |
| 192 | + onChange={(ds: DataSourceInstanceSettings) => |
| 193 | + onChange({ |
| 194 | + ...value, |
| 195 | + datasourceUid: ds.uid, |
| 196 | + }) |
| 197 | + } |
| 198 | + current={value.datasourceUid} |
| 199 | + noDefault |
| 200 | + /> |
| 201 | + </Field> |
| 202 | + )} |
| 203 | + </div> |
| 204 | + </div> |
| 205 | + ); |
| 206 | +}; |
| 207 | + |
| 208 | +const TooltipLabel = ({ content, label }: { content: string; label: string }) => ( |
| 209 | + <Label> |
| 210 | + {label} |
| 211 | + <Tooltip placement="top" content={content} theme="info"> |
| 212 | + <Icon tabIndex={0} name="info-circle" size="sm" style={{ marginLeft: '10px' }} /> |
| 213 | + </Tooltip> |
| 214 | + </Label> |
| 215 | +); |
0 commit comments