diff --git a/.changeset/clever-sources-disable.md b/.changeset/clever-sources-disable.md new file mode 100644 index 000000000..a2b9f6fd8 --- /dev/null +++ b/.changeset/clever-sources-disable.md @@ -0,0 +1,8 @@ +--- +"@hyperdx/common-utils": patch +"@hyperdx/api": patch +"@hyperdx/app": patch +--- + +Add ability to disable data sources with improved UX + diff --git a/packages/api/src/models/source.ts b/packages/api/src/models/source.ts index 68c6936a4..fcef4e29e 100644 --- a/packages/api/src/models/source.ts +++ b/packages/api/src/models/source.ts @@ -40,6 +40,10 @@ export const Source = mongoose.model( }, name: String, + disabled: { + type: Boolean, + default: false, + }, displayedTimestampValueExpression: String, implicitColumnExpression: String, serviceNameExpression: String, diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index 4e7bb649d..d727b8d2f 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -171,17 +171,22 @@ type SearchConfigFromSchema = z.infer; // Helper function to get the default source id export function getDefaultSourceId( - sources: { id: string }[] | undefined, + sources: { id: string; disabled?: boolean }[] | undefined, lastSelectedSourceId: string | undefined, ): string { if (!sources || sources.length === 0) return ''; + + // Filter out disabled sources + const enabledSources = sources.filter(s => !s.disabled); + if (enabledSources.length === 0) return ''; + if ( lastSelectedSourceId && - sources.some(s => s.id === lastSelectedSourceId) + enabledSources.some(s => s.id === lastSelectedSourceId) ) { return lastSelectedSourceId; } - return sources[0].id; + return enabledSources[0].id; } function SourceEditMenu({ diff --git a/packages/app/src/KubernetesDashboardPage.tsx b/packages/app/src/KubernetesDashboardPage.tsx index 4743c278e..131dbef91 100644 --- a/packages/app/src/KubernetesDashboardPage.tsx +++ b/packages/app/src/KubernetesDashboardPage.tsx @@ -944,7 +944,8 @@ const findSource = ( s => (kind === undefined || s.kind === kind) && (id === undefined || s.id === id) && - (connection === undefined || s.connection === connection), + (connection === undefined || s.connection === connection) && + !s.disabled, ); }; @@ -989,7 +990,8 @@ export const resolveSourceIds = ( s => s.kind === SourceKind.Log && s.metricSourceId && - findSource(sources, { id: s.metricSourceId }), + findSource(sources, { id: s.metricSourceId }) && + !s.disabled, ); if (logSourceWithMetricSource) { diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index fc13cd7ac..0316cea6d 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -1414,7 +1414,9 @@ function ServicesDashboardPage() { const appliedConfigWithoutFilters = useMemo(() => { if (!sources?.length) return appliedConfigParams; - const traceSources = sources?.filter(s => s.kind === SourceKind.Trace); + const traceSources = sources?.filter( + s => s.kind === SourceKind.Trace && !s.disabled, + ); const paramsSourceIdIsTraceSource = traceSources?.find( s => s.id === appliedConfigParams.source, ); diff --git a/packages/app/src/SessionsPage.tsx b/packages/app/src/SessionsPage.tsx index 180b6db59..5e408c8f4 100644 --- a/packages/app/src/SessionsPage.tsx +++ b/packages/app/src/SessionsPage.tsx @@ -257,9 +257,9 @@ export default function SessionsPage() { // Auto-select the first session source when the page loads useEffect(() => { if (sources && sources.length > 0 && !appliedConfig.sessionSource) { - // Find the first session source + // Find the first enabled session source const sessionSource = sources.find( - source => source.kind === SourceKind.Session, + source => source.kind === SourceKind.Session && !source.disabled, ); if (sessionSource) { setValue('source', sessionSource.id); diff --git a/packages/app/src/components/SourceSelect.tsx b/packages/app/src/components/SourceSelect.tsx index b10089053..fffa58dd3 100644 --- a/packages/app/src/components/SourceSelect.tsx +++ b/packages/app/src/components/SourceSelect.tsx @@ -66,7 +66,8 @@ function SourceSelectControlledComponent({ data ?.filter( source => - !allowedSourceKinds || allowedSourceKinds.includes(source.kind), + (!allowedSourceKinds || allowedSourceKinds.includes(source.kind)) && + !source.disabled, ) .map(d => ({ value: d.id, diff --git a/packages/app/src/components/Sources/SourceForm.tsx b/packages/app/src/components/Sources/SourceForm.tsx index 78fc712c7..97fafeb89 100644 --- a/packages/app/src/components/Sources/SourceForm.tsx +++ b/packages/app/src/components/Sources/SourceForm.tsx @@ -30,6 +30,7 @@ import { Select, Slider, Stack, + Switch, Text, Tooltip, } from '@mantine/core'; @@ -1453,6 +1454,37 @@ export function TableSourceForm({ }) { const { data: source } = useSource({ id: sourceId }); const { data: connections } = useConnections(); + const updateSourceMutation = useUpdateSource(); + + const handleDisabledToggle = useCallback( + (newDisabledValue: boolean) => { + if (!source || isNew) return; + + updateSourceMutation.mutate( + { + source: { + ...source, + disabled: newDisabledValue, + }, + }, + { + onSuccess: () => { + notifications.show({ + color: 'green', + message: `Source ${newDisabledValue ? 'disabled' : 'enabled'} successfully`, + }); + }, + onError: error => { + notifications.show({ + color: 'red', + message: `Failed to ${newDisabledValue ? 'disable' : 'enable'} source - ${error.message}`, + }); + }, + }, + ); + }, + [source, isNew, updateSourceMutation], + ); const { control, setValue, handleSubmit, resetField, setError, clearErrors } = useForm({ @@ -1824,7 +1856,27 @@ export function TableSourceForm({ } > - Source Settings + + Source Settings + {!isNew && ( + ( + { + const newDisabledValue = !event.currentTarget.checked; + onChange(newDisabledValue); + handleDisabledToggle(newDisabledValue); + }} + label={value ? 'Disabled' : 'Enabled'} + /> + )} + /> + )} + ( - -
- - {s.name} - - - - {capitalizeFirstLetter(s.kind)} - - - {connections?.find(c => c.id === s.connection)?.name} - - - {s.from && ( - <> - - {s.from.databaseName} - {s.kind === SourceKind.Metric ? '' : '.'} - {s.from.tableName} - - )} + +
+ +
+ + + {s.name} + - - + + + {capitalizeFirstLetter(s.kind)} + + + {connections?.find(c => c.id === s.connection)?.name} + + + {s.from && ( + <> + + {s.from.databaseName} + {s.kind === SourceKind.Metric ? '' : '.'} + {s.from.tableName} + + )} + + + +
+
{ if (IS_LOCAL_MODE) { - setLocalSources(prev => { - return prev.map(s => { - if (s.id === source.id) { - return source; - } - return s; - }); + const updatedSources = getLocalSources().map(s => { + if (s.id === source.id) { + return source; + } + return s; }); + setLocalSources(updatedSources); + return source; } else { return await hdxServer(`sources/${source.id}`, { method: 'PUT', @@ -151,7 +150,14 @@ export function useUpdateSource() { }); } }, - onSuccess: () => { + onSuccess: (data, variables) => { + if (IS_LOCAL_MODE) { + // Directly update the cache with the new data + queryClient.setQueryData(['sources'], (oldData: TSource[] | undefined) => { + if (!oldData) return oldData; + return oldData.map(s => s.id === variables.source.id ? variables.source : s); + }); + } queryClient.invalidateQueries({ queryKey: ['sources'] }); }, }); diff --git a/packages/common-utils/src/types.ts b/packages/common-utils/src/types.ts index 64ae03077..637163124 100644 --- a/packages/common-utils/src/types.ts +++ b/packages/common-utils/src/types.ts @@ -612,6 +612,7 @@ const SourceBaseSchema = z.object({ databaseName: z.string().min(1, 'Database is required'), tableName: z.string().min(1, 'Table is required'), }), + disabled: z.boolean().optional(), }); const RequiredTimestampColumnSchema = z