import { css } from '@emotion/css'; import pluralize from 'pluralize'; import { PureComponent } from 'react'; import * as React from 'react'; import { DropEvent, FileRejection } from 'react-dropzone'; import { QueryEditorProps, SelectableValue, rangeUtil, DataQueryRequest, DataFrameJSON, dataFrameToJSON, GrafanaTheme2, getValueFormat, formattedValueToString, Field, } from '@grafana/data'; import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { InlineField, Select, Alert, Input, InlineFieldRow, InlineLabel, FileDropzone, FileDropzoneDefaultChildren, DropzoneFile, Themeable2, withTheme2, Stack, } from '@grafana/ui'; import { hasAlphaPanels } from 'app/core/config'; import * as DFImport from 'app/features/dataframe-import'; import { getManagedChannelInfo } from 'app/features/live/info'; import { SearchQuery } from 'app/features/search/service/types'; import { GrafanaDatasource } from '../datasource'; import { defaultQuery, GrafanaQuery, GrafanaQueryType } from '../types'; import SearchEditor from './SearchEditor'; interface Props extends QueryEditorProps, Themeable2 {} const labelWidth = 12; interface State { channels: Array>; channelFields: Record>>; folders?: Array>; } export class UnthemedQueryEditor extends PureComponent { state: State = { channels: [], channelFields: {} }; queryTypes: Array> = [ { label: 'Random Walk', value: GrafanaQueryType.RandomWalk, description: 'Random signal within the selected time range', }, { label: 'Live Measurements', value: GrafanaQueryType.LiveMeasurements, description: 'Stream real-time measurements from Grafana', }, { label: 'List public files', value: GrafanaQueryType.List, description: 'Show directory listings for public resources', }, ]; constructor(props: Props) { super(props); if (config.featureToggles.panelTitleSearch && hasAlphaPanels) { this.queryTypes.push({ label: 'Search', value: GrafanaQueryType.Search, description: 'Search for grafana resources', }); } if (config.featureToggles.unifiedStorageSearch) { this.queryTypes.push({ label: 'Search (experimental)', value: GrafanaQueryType.SearchNext, description: 'Search for grafana resources', }); } if (config.featureToggles.editPanelCSVDragAndDrop) { this.queryTypes.push({ label: 'Spreadsheet or snapshot', value: GrafanaQueryType.Snapshot, description: 'Query an uploaded spreadsheet or a snapshot', }); } } loadChannelInfo() { getManagedChannelInfo().then((v) => { this.setState(v); }); } loadFolderInfo() { const query: DataQueryRequest = { targets: [{ queryType: GrafanaQueryType.List, refId: 'A' }], } as any; getDataSourceSrv() .get('-- Grafana --') .then((ds) => { const gds = ds as GrafanaDatasource; gds.query(query).subscribe({ next: (rsp) => { if (rsp.data.length) { const names: Field = rsp.data[0].fields[0]; const folders = names.values.map((v) => ({ value: v, label: v, })); this.setState({ folders }); } }, }); }); } componentDidMount() { this.loadChannelInfo(); } onQueryTypeChange = (sel: SelectableValue) => { const { onChange, query, onRunQuery } = this.props; onChange({ ...query, queryType: sel.value! }); onRunQuery(); // Reload the channel list this.loadChannelInfo(); }; onChannelChange = (sel: SelectableValue) => { const { onChange, query, onRunQuery } = this.props; onChange({ ...query, channel: sel?.value }); onRunQuery(); }; onFieldNamesChange = (item: SelectableValue) => { const { onChange, query, onRunQuery } = this.props; let fields: string[] = []; if (Array.isArray(item)) { fields = item.map((v) => v.value); } else if (item.value) { fields = [item.value]; } // When adding the first field, also add time (if it exists) if (fields.length === 1 && !query.filter?.fields?.length && query.channel) { const names = this.state.channelFields[query.channel] ?? []; const tf = names.find((f) => f.value === 'time' || f.value === 'Time'); if (tf && tf.value && tf.value !== fields[0]) { fields = [tf.value, ...fields]; } } onChange({ ...query, filter: { ...query.filter, fields, }, }); onRunQuery(); }; checkAndUpdateValue = (key: keyof GrafanaQuery, txt: string) => { const { onChange, query, onRunQuery } = this.props; if (key === 'buffer') { let buffer: number | undefined; if (txt) { try { buffer = rangeUtil.intervalToSeconds(txt) * 1000; } catch (err) { console.warn('ERROR', err); } } onChange({ ...query, buffer, }); } else { onChange({ ...query, [key]: txt, }); } onRunQuery(); }; handleEnterKey = (e: React.KeyboardEvent) => { if (e.key !== 'Enter') { return; } this.checkAndUpdateValue('buffer', e.currentTarget.value); }; handleBlur = (e: React.FocusEvent) => { this.checkAndUpdateValue('buffer', e.currentTarget.value); }; renderMeasurementsQuery() { let { channel, filter, buffer } = this.props.query; let { channels, channelFields } = this.state; let currentChannel = channels.find((c) => c.value === channel); if (channel && !currentChannel) { currentChannel = { value: channel, label: channel, description: `Connected to ${channel}`, }; channels = [currentChannel, ...channels]; } const distinctFields = new Set(); const fields: Array> = channel ? (channelFields[channel] ?? []) : []; // if (data && data.series?.length) { // for (const frame of data.series) { // for (const field of frame.fields) { // if (distinctFields.has(field.name) || !field.name) { // continue; // } // fields.push({ // value: field.name, // label: field.name, // description: `(${getFrameDisplayName(frame)} / ${field.type})`, // }); // distinctFields.add(field.name); // } // } // } if (filter?.fields) { for (const f of filter.fields) { if (!distinctFields.has(f)) { fields.push({ value: f, label: `${f} (not loaded)`, description: `Configured, but not found in the query results`, }); distinctFields.add(f); } } } let formattedTime = ''; if (buffer) { formattedTime = rangeUtil.secondsToHms(buffer / 1000); } return ( <> `Field: ${input}`} isSearchable={true} isMulti={true} /> )} This supports real-time event streams in Grafana core. This feature is under heavy development. Expect the interfaces and structures to change as this becomes more production ready. ); } onFolderChanged = (sel: SelectableValue) => { const { onChange, query, onRunQuery } = this.props; onChange({ ...query, path: sel?.value }); onRunQuery(); }; renderListPublicFiles() { let { path } = this.props.query; let { folders } = this.state; if (!folders) { folders = []; this.loadFolderInfo(); } const currentFolder = folders.find((f) => f.value === path); if (path && !currentFolder) { folders = [ ...folders, { value: path, label: path, }, ]; } return ( v.value === queryType) || queryTypes[0]} onChange={this.onQueryTypeChange} /> {queryType === GrafanaQueryType.LiveMeasurements && this.renderMeasurementsQuery()} {queryType === GrafanaQueryType.List && this.renderListPublicFiles()} {queryType === GrafanaQueryType.Snapshot && this.renderSnapshotQuery()} {queryType === GrafanaQueryType.Search && ( )} {queryType === GrafanaQueryType.SearchNext && ( )} ); } } export const QueryEditor = withTheme2(UnthemedQueryEditor); function getStyles(theme: GrafanaTheme2) { return { file: css({ width: '100%', display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: theme.spacing(2), border: `1px dashed ${theme.colors.border.medium}`, backgroundColor: theme.colors.background.secondary, marginTop: theme.spacing(1), }), }; }