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 && }
>