diff --git a/src/components/PageNavigation/PageNavigation.tsx b/src/components/PageNavigation/PageNavigation.tsx new file mode 100644 index 000000000..1479edcc4 --- /dev/null +++ b/src/components/PageNavigation/PageNavigation.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; +import { LinkButton, useStyles2 } from '@grafana/ui'; +import { css } from '@emotion/css'; + +import { AppRoutes } from 'routing/types'; +import { getRoute } from 'routing/utils'; + +export const PageNavigation = () => { + const styles = useStyles2(getStyles); + + return ( +
+
+ + Home + + + Checks + + + Probes + + + Config + +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + navigationRow: css({ + display: `flex`, + justifyContent: `flex-start`, + alignItems: `center`, + marginBottom: theme.spacing(2), + }), + stack: css({ + alignItems: `center`, + display: `flex`, + gap: theme.spacing(2), + }), +}); + + + diff --git a/src/page/CheckList/components/CheckListHeader.tsx b/src/page/CheckList/components/CheckListHeader.tsx index 01ea1324b..0b53114a2 100644 --- a/src/page/CheckList/components/CheckListHeader.tsx +++ b/src/page/CheckList/components/CheckListHeader.tsx @@ -7,6 +7,7 @@ import { CheckFiltersType, CheckListViewType, FilterType } from 'page/CheckList/ import { Check, CheckSort } from 'types'; import { getUserPermissions } from 'data/permissions'; import { AddNewCheckButton } from 'components/AddNewCheckButton'; +import { PageNavigation } from 'components/PageNavigation/PageNavigation'; import { BulkActions } from 'page/CheckList/components/BulkActions'; import { CheckFilters } from 'page/CheckList/components/CheckFilters'; import { CheckListViewSwitcher } from 'page/CheckList/components/CheckListViewSwitcher'; @@ -81,6 +82,7 @@ export const CheckListHeader = ({ return ( <> +
Currently showing {currentPageChecks.length} of {checks.length} total checks diff --git a/src/page/ConfigPageLayout/ConfigPageLayout.tsx b/src/page/ConfigPageLayout/ConfigPageLayout.tsx index 3a02eef42..c558317b1 100644 --- a/src/page/ConfigPageLayout/ConfigPageLayout.tsx +++ b/src/page/ConfigPageLayout/ConfigPageLayout.tsx @@ -7,6 +7,7 @@ import { FeatureName } from 'types'; import { AppRoutes } from 'routing/types'; import { getRoute } from 'routing/utils'; import { useFeatureFlagContext } from 'hooks/useFeatureFlagContext'; +import { PageNavigation } from 'components/PageNavigation/PageNavigation'; function getConfigTabUrl(tab = '/') { return `${getRoute(AppRoutes.Config)}/${tab}`.replace(/\/+/g, '/'); @@ -73,6 +74,7 @@ export function ConfigPageLayout() { return ( + ); diff --git a/src/page/Probes/Probes.tsx b/src/page/Probes/Probes.tsx index f2fda2c3a..6d45e2b7f 100644 --- a/src/page/Probes/Probes.tsx +++ b/src/page/Probes/Probes.tsx @@ -11,6 +11,7 @@ import { getUserPermissions } from 'data/permissions'; import { useExtendedProbes } from 'data/useProbes'; import { CenteredSpinner } from 'components/CenteredSpinner'; import { DocsLink } from 'components/DocsLink'; +import { PageNavigation } from 'components/PageNavigation/PageNavigation'; import { ProbeList } from 'components/ProbeList'; import { QueryErrorBoundary } from 'components/QueryErrorBoundary'; @@ -19,6 +20,7 @@ export const Probes = () => { return ( }> +

Probes are the agents responsible for emulating user interactions and collecting data from your specified diff --git a/src/page/SceneHomepage.tsx b/src/page/SceneHomepage.tsx index faa82076d..d347ca83c 100644 --- a/src/page/SceneHomepage.tsx +++ b/src/page/SceneHomepage.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { SceneApp, SceneAppPage } from '@grafana/scenes'; import { LoadingPlaceholder } from '@grafana/ui'; +import { PluginPage } from '@grafana/runtime'; import { DashboardSceneAppConfig } from 'types'; import { PLUGIN_URL_PATH } from 'routing/constants'; @@ -10,6 +11,7 @@ import { useLogsDS } from 'hooks/useLogsDS'; import { useMetricsDS } from 'hooks/useMetricsDS'; import { useSMDS } from 'hooks/useSMDS'; import { QueryErrorBoundary } from 'components/QueryErrorBoundary'; +import { PageNavigation } from 'components/PageNavigation/PageNavigation'; import { getSummaryScene } from 'scenes/Summary'; function SceneHomepageComponent() { @@ -31,7 +33,7 @@ function SceneHomepageComponent() { pages: [ new SceneAppPage({ title: 'Synthetics', - renderTitle: () =>

Home

, + renderTitle: () => null, // Title is rendered by PluginPage instead url: `${PLUGIN_URL_PATH}${AppRoutes.Home}`, hideFromBreadcrumbs: false, getScene: getSummaryScene(config, checks, true), @@ -44,7 +46,12 @@ function SceneHomepageComponent() { return ; } - return ; + return ( +

Home

}> + + +
+ ); } export function SceneHomepage() { diff --git a/src/scenes/components/LogsRenderer/LogsEvent.tsx b/src/scenes/components/LogsRenderer/LogsEvent.tsx index 9e802a276..6718a1067 100644 --- a/src/scenes/components/LogsRenderer/LogsEvent.tsx +++ b/src/scenes/components/LogsRenderer/LogsEvent.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; +import { Box, Text, useStyles2 } from '@grafana/ui'; import { css, cx } from '@emotion/css'; import { MSG_STRINGS_HTTP } from 'features/parseCheckLogs/checkLogs.constants.msgs'; @@ -12,38 +12,72 @@ import { UniqueLogLabels } from 'scenes/components/LogsRenderer/UniqueLogLabels' export const LogsEvent = , Record>>({ logs, mainKey, + errorLogsOnly, + onErrorLogsOnlyChange, }: { logs: T[]; mainKey: string; + errorLogsOnly: boolean; + onErrorLogsOnlyChange: (value: boolean) => void; }) => { const styles = useStyles2(getStyles); + const filteredLogs = useMemo(() => { + if (!errorLogsOnly) { + return logs; + } + return logs.filter((log) => { + const level = log.labels?.level || log.labels?.detected_level; + return level?.toLowerCase() === 'error'; + }); + }, [logs, errorLogsOnly]); + + const hasNoErrorLogs = errorLogsOnly && filteredLogs.length === 0 && logs.length > 0; + return ( -
- {logs.map((log, index) => { - const level = log.labels.detected_level; +
+ {hasNoErrorLogs ? ( + + + No error logs found. Disable the filter to see all logs. + + + ) : ( +
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((log, index) => { + const level = log.labels.detected_level; - return ( -
-
- {dateTimeFormat(log[LokiFieldNames.Time], { - defaultWithMS: true, - })} -
-
- {level.toUpperCase()} -
-
{log.labels[mainKey]}
- -
- ); - })} + return ( +
+
+ {dateTimeFormat(log[LokiFieldNames.Time], { + defaultWithMS: true, + })} +
+
+ {level.toUpperCase()} +
+
{log.labels[mainKey]}
+ +
+ ); + }) + ) : ( + + + No logs available + + + )} +
+ )}
); }; diff --git a/src/scenes/components/LogsRenderer/LogsRaw.tsx b/src/scenes/components/LogsRenderer/LogsRaw.tsx index 99a5d5a87..eb1f1b34b 100644 --- a/src/scenes/components/LogsRenderer/LogsRaw.tsx +++ b/src/scenes/components/LogsRenderer/LogsRaw.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { createDataFrame, DataFrame, @@ -11,6 +11,7 @@ import { } from '@grafana/data'; import { PanelRenderer } from '@grafana/runtime'; import { LogsDedupStrategy, LogsSortOrder } from '@grafana/schema'; +import { Box, Text } from '@grafana/ui'; import { LokiFieldNames, UnknownParsedLokiRecord } from 'features/parseLokiLogs/parseLokiLogs.types'; @@ -27,38 +28,83 @@ const logPanelOptions = { const LOGS_HEIGHT = 400; -export const LogsRaw = ({ logs }: { logs: T[] }) => { +export const LogsRaw = ({ + logs, + errorLogsOnly, + onErrorLogsOnlyChange, +}: { + logs: T[]; + errorLogsOnly: boolean; + onErrorLogsOnlyChange: (value: boolean) => void; +}) => { const [width, setWidth] = useState(0); + const filteredLogs = useMemo(() => { + if (!errorLogsOnly) { + return logs; + } + return logs.filter((log) => { + const level = log.labels?.level || log.labels?.detected_level; + return level?.toLowerCase() === 'error'; + }); + }, [logs, errorLogsOnly]); + + const hasNoErrorLogs = errorLogsOnly && filteredLogs.length === 0 && logs.length > 0; + return (
-
{ - if (el) { - setWidth(el.clientWidth); - } - }} - style={{ - height: `${LOGS_HEIGHT}px`, - }} - > - + + No error logs found. Disable the filter to see all logs. + + + ) : ( +
{ + if (el) { + setWidth(el.clientWidth); + } }} - /> -
+ style={{ + height: `${LOGS_HEIGHT}px`, + }} + > + {filteredLogs.length > 0 ? ( + + ) : ( + + + No logs available + + + )} +
+ )}
); }; const getPanelData = (logs: UnknownParsedLokiRecord[]): PanelData => { + if (logs.length === 0) { + const now = dateTime(); + return { + state: LoadingState.Done, + series: [createLogsDataFrame(logs)], + timeRange: createTimeRange(now, now), + }; + } + const firstLog = logs[0]; const lastLog = logs[logs.length - 1]; diff --git a/src/scenes/components/LogsRenderer/LogsRenderer.tsx b/src/scenes/components/LogsRenderer/LogsRenderer.tsx index 83970521c..090c82b04 100644 --- a/src/scenes/components/LogsRenderer/LogsRenderer.tsx +++ b/src/scenes/components/LogsRenderer/LogsRenderer.tsx @@ -9,17 +9,21 @@ export const LogsRenderer = ({ logs, logsView, mainKey, + errorLogsOnly, + onErrorLogsOnlyChange, }: { logs: T[]; logsView: LogsView; mainKey: string; + errorLogsOnly: boolean; + onErrorLogsOnlyChange: (value: boolean) => void; }) => { if (logsView === 'event') { - return logs={logs} mainKey={mainKey} />; + return logs={logs} mainKey={mainKey} errorLogsOnly={errorLogsOnly} onErrorLogsOnlyChange={onErrorLogsOnlyChange} />; } if (logsView === 'raw-logs') { - return logs={logs} />; + return logs={logs} errorLogsOnly={errorLogsOnly} onErrorLogsOnlyChange={onErrorLogsOnlyChange} />; } return null; diff --git a/src/scenes/components/TimepointExplorer/TimepointViewer.tsx b/src/scenes/components/TimepointExplorer/TimepointViewer.tsx index bcaf65a27..07bab81e8 100644 --- a/src/scenes/components/TimepointExplorer/TimepointViewer.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointViewer.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useRef, useState } from 'react'; import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data'; -import { Box, LoadingBar, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Box, InlineField, LoadingBar, Stack, Switch, Text, useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; import { trackTimepointViewerLogsViewToggled } from 'features/tracking/timepointExplorerEvents'; import { useResizeObserver } from 'usehooks-ts'; @@ -21,6 +21,7 @@ import { TimepointViewerExecutions } from 'scenes/components/TimepointExplorer/T export const TimepointViewer = () => { const { isInitialised, viewerState } = useTimepointExplorerContext(); const [logsView, setLogsView] = useState(LOGS_VIEW_OPTIONS[0].value); + const [errorLogsOnly, setErrorLogsOnly] = useState(false); const [viewerTimepoint, viewerProbeName] = viewerState; const styles = useStyles2(getStyles); @@ -36,10 +37,17 @@ export const TimepointViewer = () => { {viewerTimepoint ? (
- + @@ -58,11 +66,19 @@ export const TimepointViewer = () => { interface TimepointViewerContentProps { logsView: LogsView; + errorLogsOnly: boolean; + onErrorLogsOnlyChange: (value: boolean) => void; probeNameToView?: string; timepoint: StatelessTimepoint; } -const TimepointViewerContent = ({ logsView, probeNameToView, timepoint }: TimepointViewerContentProps) => { +const TimepointViewerContent = ({ + logsView, + errorLogsOnly, + onErrorLogsOnlyChange, + probeNameToView, + timepoint, +}: TimepointViewerContentProps) => { const elRef = useRef(null); const [viewerWidth, setViewerWidth] = useState(0); const { check, currentAdjustedTime } = useTimepointExplorerContext(); @@ -107,6 +123,8 @@ const TimepointViewerContent = ({ logsView, probeNameToView, timepoint }: Timepo void; + errorLogsOnly: boolean; + onErrorLogsOnlyChange: (value: boolean) => void; }) => { const styles = useStyles2(getHeaderStyles); @@ -137,7 +159,12 @@ const TimepointHeader = ({
- + + + onErrorLogsOnlyChange(e.currentTarget.checked)} /> + + +
); @@ -192,5 +219,8 @@ const getHeaderStyles = (theme: GrafanaTheme2) => { width: 100%; } `, + inlineField: css` + margin-bottom: 0; + `, }; }; diff --git a/src/scenes/components/TimepointExplorer/TimepointViewerExecutions.tsx b/src/scenes/components/TimepointExplorer/TimepointViewerExecutions.tsx index fdf896bbc..8d2066fc8 100644 --- a/src/scenes/components/TimepointExplorer/TimepointViewerExecutions.tsx +++ b/src/scenes/components/TimepointExplorer/TimepointViewerExecutions.tsx @@ -23,6 +23,8 @@ import { useTimepointViewerExecutions } from 'scenes/components/TimepointExplore interface TimepointViewerExecutionsProps { isLoading: boolean; logsView: LogsView; + errorLogsOnly: boolean; + onErrorLogsOnlyChange: (value: boolean) => void; pendingProbeNames: string[]; probeExecutions: ProbeExecutionLogs[]; probeNameToView?: string; @@ -32,6 +34,8 @@ interface TimepointViewerExecutionsProps { export const TimepointViewerExecutions = ({ isLoading, logsView, + errorLogsOnly, + onErrorLogsOnlyChange, pendingProbeNames, probeExecutions = [], probeNameToView, @@ -103,7 +107,15 @@ export const TimepointViewerExecutions = ({ } if (executions.length > 1) { - return ; + return ( + + ); } return ( @@ -115,6 +127,8 @@ export const TimepointViewerExecutions = ({ logs={execution} logsView={logsView} mainKey="msg" + errorLogsOnly={errorLogsOnly} + onErrorLogsOnlyChange={onErrorLogsOnlyChange} /> ); })} @@ -176,7 +190,17 @@ const ProbeNameIcon = ({ status }: { status: TimepointStatus }) => { return ; }; -const MultipleExecutions = ({ executions, logsView }: { executions: ExecutionLogs[]; logsView: LogsView }) => { +const MultipleExecutions = ({ + executions, + logsView, + errorLogsOnly, + onErrorLogsOnlyChange, +}: { + executions: ExecutionLogs[]; + logsView: LogsView; + errorLogsOnly: boolean; + onErrorLogsOnlyChange: (value: boolean) => void; +}) => { const styles = useStyles2(getStyles); const success = useTimepointVizOptions('success'); const failure = useTimepointVizOptions('failure'); @@ -202,7 +226,13 @@ const MultipleExecutions = ({ executions, logsView }: { executions: ExecutionLog color={`${probe_success === '1' ? success.statusColor : failure.statusColor}`} /> - logs={execution} logsView={logsView} mainKey="msg" /> + + logs={execution} + logsView={logsView} + mainKey="msg" + errorLogsOnly={errorLogsOnly} + onErrorLogsOnlyChange={onErrorLogsOnlyChange} + />
{index !== executions.length - 1 &&
}