Skip to content

Commit 15331ac

Browse files
authored
feat: Auto-select correlated sources on k8s dashboard (#1302)
Closes HDX-2586 # Summary This PR updates the K8s dashboard so that it auto-selects correlated log and metric sources. Auto-selection of sources happens 1. During page load, if sources aren't specified in the URL params 2. When a new log source is selected, a correlated metric source is auto-selected. In this case, a notification is shown to the user to inform them that the metric source has been updated. When a new metric source is selected, a correlated log source is not selected. This is to ensure the user has some way of selecting two non-correlated sources, if they truly want to. If the user does select a metric source which is not correlated with the selected log source, a warning notification will be shown to the user. ## Demo https://github.com/user-attachments/assets/492121a1-0a51-4af9-a749-42771537678e
1 parent 757196f commit 15331ac

File tree

3 files changed

+289
-77
lines changed

3 files changed

+289
-77
lines changed

.changeset/funny-games-cheer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
feat: Auto-select correlated sources on k8s dashboard

packages/app/src/KubernetesDashboardPage.tsx

Lines changed: 127 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import cx from 'classnames';
66
import sub from 'date-fns/sub';
77
import { useQueryState } from 'nuqs';
88
import { useForm } from 'react-hook-form';
9-
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
109
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
1110
import {
1211
Badge,
@@ -24,6 +23,7 @@ import {
2423
Text,
2524
Tooltip,
2625
} from '@mantine/core';
26+
import { notifications } from '@mantine/notifications';
2727

2828
import { TimePicker } from '@/components/TimePicker';
2929

@@ -757,57 +757,97 @@ const defaultTimeRange = parseTimeQuery('Past 1h', false);
757757

758758
const CHART_HEIGHT = 300;
759759

760+
const findSource = (
761+
sources: TSource[] | undefined,
762+
filters: {
763+
kind?: SourceKind;
764+
connection?: string;
765+
id?: string;
766+
},
767+
) => {
768+
if (!sources) return undefined;
769+
770+
const { kind, connection, id } = filters;
771+
return sources.find(
772+
s =>
773+
(kind === undefined || s.kind === kind) &&
774+
(id === undefined || s.id === id) &&
775+
(connection === undefined || s.connection === connection),
776+
);
777+
};
778+
760779
export const resolveSourceIds = (
761780
_logSourceId: string | null | undefined,
762781
_metricSourceId: string | null | undefined,
763782
sources: TSource[] | undefined,
764783
) => {
765-
if (_logSourceId && _metricSourceId) {
766-
return [_logSourceId, _metricSourceId];
784+
if ((_logSourceId && _metricSourceId) || !sources) {
785+
return {
786+
logSourceId: _logSourceId ?? undefined,
787+
metricSourceId: _metricSourceId ?? undefined,
788+
};
767789
}
768790

769-
// Default the metric source to the first one from the same connection as the log source
770-
if (_logSourceId && !_metricSourceId && sources) {
771-
const { connection } = sources.find(s => s.id === _logSourceId) ?? {};
772-
const metricSource = sources.find(
773-
s => s.connection === connection && s.kind === SourceKind.Metric,
774-
);
775-
return [_logSourceId, metricSource?.id];
791+
// Find a default metric source that matches the existing log source
792+
if (_logSourceId && !_metricSourceId) {
793+
const { connection, metricSourceId: correlatedMetricSourceId } =
794+
findSource(sources, { id: _logSourceId }) ?? {};
795+
const metricSourceId =
796+
(correlatedMetricSourceId &&
797+
findSource(sources, { id: correlatedMetricSourceId })?.id) ??
798+
(connection &&
799+
findSource(sources, { connection, kind: SourceKind.Metric })?.id);
800+
return { logSourceId: _logSourceId, metricSourceId };
776801
}
777802

778-
// Default the log source to the first one from the same connection as the metric source
779-
if (!_logSourceId && _metricSourceId && sources) {
780-
const { connection } = sources.find(s => s.id === _metricSourceId) ?? {};
781-
const logSource = sources.find(
782-
s => s.connection === connection && s.kind === SourceKind.Log,
783-
);
784-
return [logSource?.id, _metricSourceId];
803+
// Find a default log source that matches the existing metric source
804+
if (!_logSourceId && _metricSourceId) {
805+
const { connection, logSourceId: correlatedLogSourceId } =
806+
findSource(sources, { id: _metricSourceId }) ?? {};
807+
const logSourceId =
808+
(correlatedLogSourceId &&
809+
findSource(sources, { id: correlatedLogSourceId })?.id) ??
810+
(connection &&
811+
findSource(sources, { connection, kind: SourceKind.Log })?.id);
812+
return { logSourceId, metricSourceId: _metricSourceId };
785813
}
786814

787-
// Find a Log and Metric source from the same connection
788-
if (sources) {
789-
const connections = sources.map(s => s.connection);
790-
const connectionWithBothSourceKinds = connections.find(
791-
conn =>
792-
sources.some(s => s.connection === conn && s.kind === SourceKind.Log) &&
793-
sources.some(
794-
s => s.connection === conn && s.kind === SourceKind.Metric,
795-
),
796-
);
797-
const logSource = sources.find(
798-
s =>
799-
s.connection === connectionWithBothSourceKinds &&
800-
s.kind === SourceKind.Log,
801-
);
802-
const metricSource = sources.find(
803-
s =>
804-
s.connection === connectionWithBothSourceKinds &&
805-
s.kind === SourceKind.Metric,
806-
);
807-
return [logSource?.id, metricSource?.id];
815+
// Find any two correlated log and metric sources
816+
const logSourceWithMetricSource = sources.find(
817+
s =>
818+
s.kind === SourceKind.Log &&
819+
s.metricSourceId &&
820+
findSource(sources, { id: s.metricSourceId }),
821+
);
822+
823+
if (logSourceWithMetricSource) {
824+
return {
825+
logSourceId: logSourceWithMetricSource.id,
826+
metricSourceId: logSourceWithMetricSource.metricSourceId,
827+
};
808828
}
809829

810-
return [_logSourceId, _metricSourceId];
830+
// Find a Log and Metric source from the same connection
831+
const connections = Array.from(new Set(sources.map(s => s.connection)));
832+
const connectionWithBothSourceKinds = connections.find(
833+
connection =>
834+
findSource(sources, { connection, kind: SourceKind.Log }) &&
835+
findSource(sources, { connection, kind: SourceKind.Metric }),
836+
);
837+
const logSource = connectionWithBothSourceKinds
838+
? findSource(sources, {
839+
connection: connectionWithBothSourceKinds,
840+
kind: SourceKind.Log,
841+
})
842+
: undefined;
843+
const metricSource = connectionWithBothSourceKinds
844+
? findSource(sources, {
845+
connection: connectionWithBothSourceKinds,
846+
kind: SourceKind.Metric,
847+
})
848+
: undefined;
849+
850+
return { logSourceId: logSource?.id, metricSourceId: metricSource?.id };
811851
};
812852

813853
function KubernetesDashboardPage() {
@@ -816,7 +856,7 @@ function KubernetesDashboardPage() {
816856
const [_logSourceId, setLogSourceId] = useQueryState('logSource');
817857
const [_metricSourceId, setMetricSourceId] = useQueryState('metricSource');
818858

819-
const [logSourceId, metricSourceId] = useMemo(
859+
const { logSourceId, metricSourceId } = useMemo(
820860
() => resolveSourceIds(_logSourceId, _metricSourceId, sources),
821861
[_logSourceId, _metricSourceId, sources],
822862
);
@@ -846,22 +886,59 @@ function KubernetesDashboardPage() {
846886
watch((data, { name, type }) => {
847887
if (name === 'logSourceId' && type === 'change') {
848888
setLogSourceId(data.logSourceId ?? null);
889+
890+
// Default to the log source's correlated metric source
891+
if (data.logSourceId && sources) {
892+
const logSource = findSource(sources, { id: data.logSourceId });
893+
const correlatedMetricSource = logSource?.metricSourceId
894+
? findSource(sources, { id: logSource.metricSourceId })
895+
: undefined;
896+
if (
897+
correlatedMetricSource &&
898+
correlatedMetricSource.id !== data.metricSourceId
899+
) {
900+
setMetricSourceId(correlatedMetricSource.id);
901+
notifications.show({
902+
id: `${correlatedMetricSource.id}-auto-correlated-metric-source`,
903+
title: 'Updated Metrics Source',
904+
message: `Using correlated metrics source: ${correlatedMetricSource.name}`,
905+
});
906+
} else if (logSource && !correlatedMetricSource) {
907+
notifications.show({
908+
id: `${logSource.id}-not-correlated`,
909+
title: 'Warning',
910+
message: `The selected logs source is not correlated with a metrics source. Source correlations can be configured in Team Settings.`,
911+
color: 'yellow',
912+
});
913+
}
914+
}
849915
} else if (name === 'metricSourceId' && type === 'change') {
850916
setMetricSourceId(data.metricSourceId ?? null);
917+
const metricSource = data.metricSourceId
918+
? findSource(sources, { id: data.metricSourceId })
919+
: undefined;
920+
if (
921+
metricSource &&
922+
data.logSourceId &&
923+
metricSource.logSourceId !== data.logSourceId
924+
) {
925+
notifications.show({
926+
id: `${metricSource.id}-not-correlated`,
927+
title: 'Warning',
928+
message: `The selected metrics source is not correlated with the selected logs source. Source correlations can be configured in Team Settings.`,
929+
color: 'yellow',
930+
});
931+
}
851932
}
852933
});
853934

854-
const [activeTab, setActiveTab] = useQueryParam(
855-
'tab',
856-
withDefault(StringParam, 'pods'),
857-
{ updateType: 'replaceIn' },
858-
);
935+
const [activeTab, setActiveTab] = useQueryState('tab', {
936+
defaultValue: 'pods',
937+
});
859938

860-
const [searchQuery, setSearchQuery] = useQueryParam(
861-
'q',
862-
withDefault(StringParam, ''),
863-
{ updateType: 'replaceIn' },
864-
);
939+
const [searchQuery, setSearchQuery] = useQueryState('q', {
940+
defaultValue: '',
941+
});
865942

866943
const {
867944
searchedTimeRange: dateRange,

0 commit comments

Comments
 (0)