From 8761dfcc9763644e0ec2b18567a3fa2ef63ad504 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:32:20 +0100 Subject: [PATCH] Add custom alert component (#81012) Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Matias Chomicki --- .../src/TemporaryAlert.tsx | 51 +++++++ .../grafana-o11y-ds-frontend/src/index.ts | 1 + .../tempo/traceql/TraceQLEditor.tsx | 143 +++++++++--------- 3 files changed, 126 insertions(+), 69 deletions(-) create mode 100644 packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx diff --git a/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx b/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx new file mode 100644 index 00000000000..3f7b6961e5b --- /dev/null +++ b/packages/grafana-o11y-ds-frontend/src/TemporaryAlert.tsx @@ -0,0 +1,51 @@ +import { css } from '@emotion/css'; +import React, { useEffect, useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Alert, AlertVariant, useTheme2 } from '@grafana/ui'; + +enum AlertTimeout { + Error = 7000, + Info = 3000, + Success = 3000, + Warning = 5000, +} + +const getStyle = (theme: GrafanaTheme2) => { + return css({ + position: 'absolute', + zIndex: theme.zIndex.portal, + top: 0, + right: 10, + }); +}; + +const timeoutMap = { + ['error']: AlertTimeout.Error, + ['info']: AlertTimeout.Info, + ['success']: AlertTimeout.Success, + ['warning']: AlertTimeout.Warning, +}; + +export const TemporaryAlert = (props: { severity: AlertVariant; text: string }) => { + const style = getStyle(useTheme2()); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setVisible(false); + }, timeoutMap[props.severity]); + + return () => { + clearTimeout(timer); + }; + }, [props.severity, visible]); + + useEffect(() => { + if (props.text !== '') { + setVisible(true); + } + }, [props.text]); + + return ; +}; diff --git a/packages/grafana-o11y-ds-frontend/src/index.ts b/packages/grafana-o11y-ds-frontend/src/index.ts index 591a0cc5b5c..18bd6e95419 100644 --- a/packages/grafana-o11y-ds-frontend/src/index.ts +++ b/packages/grafana-o11y-ds-frontend/src/index.ts @@ -7,6 +7,7 @@ export * from './IntervalInput/IntervalInput'; export * from './NodeGraph/NodeGraphSettings'; export * from './SpanBar/SpanBarSettings'; +export * from './TemporaryAlert'; export * from './TraceToLogs/TagMappingInput'; export * from './TraceToLogs/TraceToLogsSettings'; export * from './TraceToMetrics/TraceToMetricsSettings'; diff --git a/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx b/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx index 64aa15acfbd..5d5e001395c 100644 --- a/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx @@ -1,13 +1,11 @@ import { css } from '@emotion/css'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { TemporaryAlert } from '@grafana/o11y-ds-frontend'; import { reportInteraction } from '@grafana/runtime'; import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui'; -import { notifyApp } from '../_importedDependencies/actions/appNotification'; -import { createErrorNotification } from '../_importedDependencies/core/appNotification'; -import { dispatch } from '../_importedDependencies/store'; import { TempoDatasource } from '../datasource'; import { CompletionProvider, CompletionType } from './autocomplete'; @@ -24,8 +22,10 @@ interface Props { } export function TraceQLEditor(props: Props) { + const [alertText, setAlertText] = useState('error error'); + const { onChange, onRunQuery, placeholder } = props; - const setupAutocompleteFn = useAutocomplete(props.datasource); + const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText); const theme = useTheme2(); const styles = getStyles(theme, placeholder); // work around the problem that `onEditorDidMount` is called once @@ -36,73 +36,76 @@ export function TraceQLEditor(props: Props) { const errorTimeoutId = useRef(); return ( - { - if (!props.readOnly) { - setupAutocompleteFn(editor, monaco, setupRegisterInteractionCommand(editor)); - setupActions(editor, monaco, () => onRunQueryRef.current()); - setupPlaceholder(editor, monaco, styles); - } - setupAutoSize(editor); + <> + { + if (!props.readOnly) { + setupAutocompleteFn(editor, monaco, setupRegisterInteractionCommand(editor)); + setupActions(editor, monaco, () => onRunQueryRef.current()); + setupPlaceholder(editor, monaco, styles); + } + setupAutoSize(editor); - // Parse query that might already exist (e.g., after a page refresh) - const model = editor.getModel(); - if (model) { - const errorNodes = getErrorNodes(model.getValue()); - setMarkers(monaco, model, errorNodes); - } - - // Register callback for query changes - editor.onDidChangeModelContent((changeEvent) => { + // Parse query that might already exist (e.g., after a page refresh) const model = editor.getModel(); - - if (!model) { - return; + if (model) { + const errorNodes = getErrorNodes(model.getValue()); + setMarkers(monaco, model, errorNodes); } - // Remove previous callback if existing, to prevent squiggles from been shown while the user is still typing - window.clearTimeout(errorTimeoutId.current); + // Register callback for query changes + editor.onDidChangeModelContent((changeEvent) => { + const model = editor.getModel(); - const errorNodes = getErrorNodes(model.getValue()); - const cursorPosition = changeEvent.changes[0].rangeOffset; + if (!model) { + return; + } - // Immediately updates the squiggles, in case the user fixed an error, - // excluding the error around the cursor position - setMarkers( - monaco, - model, - errorNodes.filter((errorNode) => !(errorNode.from <= cursorPosition && cursorPosition <= errorNode.to)) - ); + // Remove previous callback if existing, to prevent squiggles from been shown while the user is still typing + window.clearTimeout(errorTimeoutId.current); - // Later on, show all errors - errorTimeoutId.current = window.setTimeout(() => { - setMarkers(monaco, model, errorNodes); - }, 500); - }); - }} - /> + const errorNodes = getErrorNodes(model.getValue()); + const cursorPosition = changeEvent.changes[0].rangeOffset; + + // Immediately updates the squiggles, in case the user fixed an error, + // excluding the error around the cursor position + setMarkers( + monaco, + model, + errorNodes.filter((errorNode) => !(errorNode.from <= cursorPosition && cursorPosition <= errorNode.to)) + ); + + // Later on, show all errors + errorTimeoutId.current = window.setTimeout(() => { + setMarkers(monaco, model, errorNodes); + }, 500); + }); + }} + /> + {alertText && } + ); } @@ -175,9 +178,10 @@ function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) { /** * Hook that returns function that will set up monaco autocomplete for the label selector - * @param datasource + * @param datasource the Tempo datasource instance + * @param setAlertText setter for alert's text */ -function useAutocomplete(datasource: TempoDatasource) { +function useAutocomplete(datasource: TempoDatasource, setAlertText: (text: string) => void) { // We need the provider ref so we can pass it the label/values data later. This is because we run the call for the // values here but there is additional setup needed for the provider later on. We could run the getSeries() in the // returned function but that is run after the monaco is mounted so would delay the request a bit when it does not @@ -192,12 +196,13 @@ function useAutocomplete(datasource: TempoDatasource) { await datasource.languageProvider.start(); } catch (error) { if (error instanceof Error) { - dispatch(notifyApp(createErrorNotification('Error', error))); + console.error(error); + setAlertText(error.message); } } }; fetchTags(); - }, [datasource]); + }, [datasource, setAlertText]); const autocompleteDisposeFun = useRef<(() => void) | null>(null); useEffect(() => {