Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/app/src/KubernetesDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import DBRowSidePanel from './components/DBRowSidePanel';
import { DBSqlRowTable } from './components/DBRowTable';
import { DBTimeChart } from './components/DBTimeChart';
import { FormatPodStatus } from './components/KubeComponents';
import { KubernetesFilters } from './components/KubernetesFilters';
import OnboardingModal from './components/OnboardingModal';
import { useQueriedChartConfig } from './hooks/useChartConfig';
import {
Expand Down Expand Up @@ -942,6 +943,15 @@ function KubernetesDashboardPage() {
/>
</form>
</Group>
{metricSource && (
<KubernetesFilters
dateRange={dateRange}
metricSource={metricSource}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
)}

<Tabs
mt="md"
keepMounted={false}
Expand Down
245 changes: 245 additions & 0 deletions packages/app/src/components/KubernetesFilters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
ChartConfigWithDateRange,
TSource,
} from '@hyperdx/common-utils/dist/types';
import { Group, Select } from '@mantine/core';

import { useGetKeyValues } from '@/hooks/useMetadata';
import SearchInputV2 from '@/SearchInputV2';

type KubernetesFiltersProps = {
dateRange: [Date, Date];
metricSource: TSource;
searchQuery: string;
setSearchQuery: (query: string) => void;
};

type FilterSelectProps = {
metricSource: TSource;
placeholder: string;
fieldName: string;
value: string | null;
onChange: (value: string | null) => void;
chartConfig: ChartConfigWithDateRange;
};

const FilterSelect: React.FC<FilterSelectProps> = ({
metricSource,
placeholder,
fieldName,
value,
onChange,
chartConfig,
}) => {
const { data, isLoading } = useGetKeyValues({
chartConfigs: chartConfig,
keys: [`${metricSource.resourceAttributesExpression}['${fieldName}']`],
});

return (
<Select
placeholder={placeholder + (isLoading ? ' (loading...)' : '')}
data={data?.[0]?.value.map(value => ({ value, label: value })) || []}
value={value}
onChange={onChange}
searchable
clearable
allowDeselect
size="xs"
maxDropdownHeight={280}
disabled={isLoading}
variant="filled"
w={200}
limit={20}
/>
);
};

export const KubernetesFilters: React.FC<KubernetesFiltersProps> = ({
dateRange,
metricSource,
searchQuery,
setSearchQuery,
}) => {
// State for each filter
const [podName, setPodName] = useState<string | null>(null);
const [deploymentName, setDeploymentName] = useState<string | null>(null);
const [nodeName, setNodeName] = useState<string | null>(null);
const [namespaceName, setNamespaceName] = useState<string | null>(null);
const [clusterName, setClusterName] = useState<string | null>(null);

const { control, watch, setValue } = useForm({
defaultValues: {
searchQuery: searchQuery,
},
});

// Watch for changes in the search query field
useEffect(() => {
const subscription = watch((value, { name }) => {
if (name === 'searchQuery') {
setSearchQuery(value.searchQuery ?? '');
}
});
return () => subscription.unsubscribe();
}, [watch, setSearchQuery]);

// Update search form value when search query changes
useEffect(() => {
setValue('searchQuery', searchQuery);
}, [searchQuery, setValue]);

// Helper function to extract value from search query
const extractValueFromSearchQuery = (
searchQuery: string,
resourceAttr: string = '',
attribute: string,
) => {
const match = searchQuery.match(
new RegExp(`${resourceAttr}\\.${attribute}:"([^"]+)"`, 'i'),
);
return match ? match[1] : null;
};

// Initialize filter values from search query
useEffect(() => {
if (searchQuery) {
const resourceAttr = metricSource.resourceAttributesExpression;

setPodName(
extractValueFromSearchQuery(searchQuery, resourceAttr, 'k8s.pod.name'),
);
setDeploymentName(
extractValueFromSearchQuery(
searchQuery,
resourceAttr,
'k8s.deployment.name',
),
);
setNodeName(
extractValueFromSearchQuery(searchQuery, resourceAttr, 'k8s.node.name'),
);
setNamespaceName(
extractValueFromSearchQuery(
searchQuery,
resourceAttr,
'k8s.namespace.name',
),
);
setClusterName(
extractValueFromSearchQuery(
searchQuery,
resourceAttr,
'k8s.cluster.name',
),
);
}
}, [searchQuery, metricSource.resourceAttributesExpression]);

// Create chart config for fetching key values
const chartConfig: ChartConfigWithDateRange = {
from: {
databaseName: metricSource.from.databaseName,
tableName: metricSource.metricTables?.gauge || '',
},
where: '',
whereLanguage: 'sql',
select: '',
timestampValueExpression: metricSource.timestampValueExpression || '',
connection: metricSource.connection,
dateRange,
};

// Helper function to update search query
const updateSearchQuery = (
attribute: string,
value: string | null,
setter: (value: string | null) => void,
) => {
setter(value);

const resourceAttr = metricSource.resourceAttributesExpression;
const fullAttribute = `${resourceAttr}.${attribute}`;

// Remove existing filter for this attribute if it exists
let newQuery = searchQuery;
const regex = new RegExp(`${fullAttribute}:"[^"]*"`, 'g');
newQuery = newQuery.replace(regex, '').trim();

// Add new filter if value is not null
if (value) {
newQuery = `${fullAttribute}:"${value}" ${newQuery}`.trim();
}

setSearchQuery(newQuery);
};

return (
<Group mt="md" mb="xs" wrap="wrap" gap="xxs">
<FilterSelect
metricSource={metricSource}
placeholder="Pod"
fieldName="k8s.pod.name"
value={podName}
onChange={value => updateSearchQuery('k8s.pod.name', value, setPodName)}
chartConfig={chartConfig}
/>

<FilterSelect
metricSource={metricSource}
placeholder="Deployment"
fieldName="k8s.deployment.name"
value={deploymentName}
onChange={value =>
updateSearchQuery('k8s.deployment.name', value, setDeploymentName)
}
chartConfig={chartConfig}
/>

<FilterSelect
metricSource={metricSource}
placeholder="Node"
fieldName="k8s.node.name"
value={nodeName}
onChange={value =>
updateSearchQuery('k8s.node.name', value, setNodeName)
}
chartConfig={chartConfig}
/>

<FilterSelect
metricSource={metricSource}
placeholder="Namespace"
fieldName="k8s.namespace.name"
value={namespaceName}
onChange={value =>
updateSearchQuery('k8s.namespace.name', value, setNamespaceName)
}
chartConfig={chartConfig}
/>

<FilterSelect
metricSource={metricSource}
placeholder="Cluster"
fieldName="k8s.cluster.name"
value={clusterName}
onChange={value =>
updateSearchQuery('k8s.cluster.name', value, setClusterName)
}
chartConfig={chartConfig}
/>
<SearchInputV2
tableConnections={tcFromSource(metricSource)}
placeholder="Search query"
language="lucene"
name="searchQuery"
control={control}
size="xs"
enableHotkey
/>
</Group>
);
};