mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
chore: update watch uses to useWatch (#1516)
Fixes: HDX-3101 Co-authored-by: Brandon Pereira <7552738+brandon-pereira@users.noreply.github.com>
This commit is contained in:
parent
6537884825
commit
468eb92481
29 changed files with 687 additions and 428 deletions
5
.changeset/yellow-files-deny.md
Normal file
5
.changeset/yellow-files-deny.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
Update some forms to work better with React 19
|
||||
|
|
@ -8,6 +8,8 @@ import prettierConfig from 'eslint-config-prettier';
|
|||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||
import prettierPlugin from 'eslint-plugin-prettier/recommended';
|
||||
import playwrightPlugin from 'eslint-plugin-playwright';
|
||||
import reactHookFormPlugin from 'eslint-plugin-react-hook-form';
|
||||
import { fixupPluginRules } from '@eslint/compat';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
|
|
@ -39,6 +41,7 @@ export default [
|
|||
react: reactPlugin,
|
||||
'react-hooks': reactHooksPlugin,
|
||||
'simple-import-sort': simpleImportSort,
|
||||
'react-hook-form': fixupPluginRules(reactHookFormPlugin), // not compatible with eslint 9 yet
|
||||
},
|
||||
rules: {
|
||||
...nextPlugin.configs.recommended.rules,
|
||||
|
|
@ -81,6 +84,7 @@ export default [
|
|||
],
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'react-hook-form/no-use-watch': 'error',
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^4.1.3",
|
||||
"@eslint/compat": "^2.0.0",
|
||||
"@hookform/devtools": "^4.3.1",
|
||||
"@jedmao/location": "^3.0.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
|
|
@ -135,6 +136,7 @@
|
|||
"@types/sqlstring": "^2.3.2",
|
||||
"eslint-config-next": "^16.0.10",
|
||||
"eslint-plugin-playwright": "^2.4.0",
|
||||
"eslint-plugin-react-hook-form": "^0.3.1",
|
||||
"eslint-plugin-storybook": "10.1.4",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^30.2.0",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import Link from 'next/link';
|
|||
import { useRouter } from 'next/router';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import cx from 'classnames';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||
import {
|
||||
Button,
|
||||
Notification,
|
||||
|
|
@ -45,7 +45,7 @@ export default function AuthPage({ action }: { action: 'register' | 'login' }) {
|
|||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
setError,
|
||||
watch,
|
||||
control,
|
||||
} = useForm<FormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
});
|
||||
|
|
@ -67,8 +67,16 @@ export default function AuthPage({ action }: { action: 'register' | 'login' }) {
|
|||
}
|
||||
}, [installation, isRegister, router]);
|
||||
|
||||
const currentPassword = watch('password', '');
|
||||
const confirmPassword = watch('confirmPassword', '');
|
||||
const currentPassword = useWatch({
|
||||
control,
|
||||
name: 'password',
|
||||
defaultValue: '',
|
||||
});
|
||||
const confirmPassword = useWatch({
|
||||
control,
|
||||
name: 'confirmPassword',
|
||||
defaultValue: '',
|
||||
});
|
||||
|
||||
const confirmPass = () => {
|
||||
return currentPassword === confirmPassword;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import {
|
||||
parseAsFloat,
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
useQueryState,
|
||||
useQueryStates,
|
||||
} from 'nuqs';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { sql } from '@codemirror/lang-sql';
|
||||
import { format as formatSql } from '@hyperdx/common-utils/dist/sqlFormatter';
|
||||
import { DisplayType } from '@hyperdx/common-utils/dist/types';
|
||||
|
|
@ -436,17 +436,19 @@ function ClickhousePage() {
|
|||
|
||||
const connection = _connection ?? connections?.[0]?.id ?? '';
|
||||
|
||||
const { control, watch } = useForm({
|
||||
const { control } = useForm({
|
||||
values: {
|
||||
connection,
|
||||
},
|
||||
});
|
||||
|
||||
watch((data, { name, type }) => {
|
||||
if (name === 'connection' && type === 'change') {
|
||||
setConnection(data.connection ?? null);
|
||||
const watchedConnection = useWatch({ control, name: 'connection' });
|
||||
|
||||
useEffect(() => {
|
||||
if (watchedConnection !== connection) {
|
||||
setConnection(watchedConnection ?? null);
|
||||
}
|
||||
});
|
||||
}, [watchedConnection, connection, setConnection]);
|
||||
const DEFAULT_INTERVAL = 'Past 1h';
|
||||
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
|
||||
useState(DEFAULT_INTERVAL);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from 'react';
|
|||
import dynamic from 'next/dynamic';
|
||||
import Head from 'next/head';
|
||||
import { parseAsJson, parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { SavedChartConfig, SourceKind } from '@hyperdx/common-utils/dist/types';
|
||||
import {
|
||||
|
|
@ -57,7 +57,7 @@ function AIAssistant({
|
|||
'ai-assistant-alert-dismissed',
|
||||
false,
|
||||
);
|
||||
const { control, watch, setValue, handleSubmit } = useForm<{
|
||||
const { control, setValue, handleSubmit } = useForm<{
|
||||
text: string;
|
||||
source: string;
|
||||
}>({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import dynamic from 'next/dynamic';
|
|||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { filter } from 'lodash';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
|
@ -191,7 +191,7 @@ function Mapping({ input }: { input: Input }) {
|
|||
const { data: sources } = useSources();
|
||||
const [dashboardId] = useQueryParam('dashboardId', StringParam);
|
||||
|
||||
const { handleSubmit, getFieldState, control, setValue, watch } =
|
||||
const { handleSubmit, getFieldState, control, setValue } =
|
||||
useForm<SourceResolutionFormValues>({
|
||||
resolver: zodResolver(SourceResolutionForm),
|
||||
defaultValues: {
|
||||
|
|
@ -226,16 +226,24 @@ function Mapping({ input }: { input: Input }) {
|
|||
}, [setValue, sources, input]);
|
||||
|
||||
const isUpdatingRef = useRef(false);
|
||||
watch((a, { name }) => {
|
||||
if (isUpdatingRef.current) return;
|
||||
if (!a.sourceMappings || !input.tiles) return;
|
||||
const [, inputIdx] = name?.split('.') || [];
|
||||
if (!inputIdx) return;
|
||||
const sourceMappings = useWatch({ control, name: 'sourceMappings' });
|
||||
const prevSourceMappingsRef = useRef(sourceMappings);
|
||||
|
||||
const idx = Number(inputIdx);
|
||||
const inputTile = input.tiles[idx];
|
||||
useEffect(() => {
|
||||
if (isUpdatingRef.current) return;
|
||||
if (!sourceMappings || !input.tiles) return;
|
||||
|
||||
// Find which mapping changed
|
||||
const changedIdx = sourceMappings.findIndex(
|
||||
(mapping, idx) => mapping !== prevSourceMappingsRef.current?.[idx],
|
||||
);
|
||||
if (changedIdx === -1) return;
|
||||
|
||||
prevSourceMappingsRef.current = sourceMappings;
|
||||
|
||||
const inputTile = input.tiles[changedIdx];
|
||||
if (!inputTile) return;
|
||||
const sourceId = a.sourceMappings[idx] ?? '';
|
||||
const sourceId = sourceMappings[changedIdx] ?? '';
|
||||
const keysForTilesWithMatchingSource = input.tiles
|
||||
.map((tile, index) => ({ ...tile, index }))
|
||||
.filter(tile => tile.config.source === inputTile.config.source)
|
||||
|
|
@ -263,7 +271,7 @@ function Mapping({ input }: { input: Input }) {
|
|||
}
|
||||
|
||||
isUpdatingRef.current = false;
|
||||
});
|
||||
}, [sourceMappings, input.tiles, input.filters, getFieldState, setValue]);
|
||||
|
||||
const createDashboard = useCreateDashboard();
|
||||
const updateDashboard = useUpdateDashboard();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import produce from 'immer';
|
|||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import RGL, { WidthProvider } from 'react-grid-layout';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import { TableConnection } from '@hyperdx/common-utils/dist/core/metadata';
|
||||
import { convertToDashboardTemplate } from '@hyperdx/common-utils/dist/core/utils';
|
||||
import {
|
||||
|
|
@ -644,7 +644,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
|
||||
const [isLive, setIsLive] = useState(false);
|
||||
|
||||
const { control, watch, setValue, handleSubmit } = useForm<{
|
||||
const { control, setValue, handleSubmit } = useForm<{
|
||||
granularity: SQLInterval | 'auto';
|
||||
where: SearchCondition;
|
||||
whereLanguage: SearchConditionLanguage;
|
||||
|
|
@ -660,11 +660,14 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
whereLanguage: (whereLanguage as SearchConditionLanguage) ?? 'lucene',
|
||||
},
|
||||
});
|
||||
watch((data, { name, type }) => {
|
||||
if (name === 'granularity' && type === 'change') {
|
||||
setGranularity(data.granularity as SQLInterval);
|
||||
|
||||
const watchedGranularity = useWatch({ control, name: 'granularity' });
|
||||
|
||||
useEffect(() => {
|
||||
if (watchedGranularity && watchedGranularity !== granularity) {
|
||||
setGranularity(watchedGranularity as SQLInterval);
|
||||
}
|
||||
});
|
||||
}, [watchedGranularity, granularity, setGranularity]);
|
||||
|
||||
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
|
||||
useState('Past 1h');
|
||||
|
|
|
|||
|
|
@ -843,7 +843,6 @@ function DBSearchPage() {
|
|||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
handleSubmit,
|
||||
|
|
@ -1004,31 +1003,31 @@ function DBSearchPage() {
|
|||
onFilterChange: handleSetFilters,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = watch((data, { name, type }) => {
|
||||
// If the user changes the source dropdown, reset the select and orderby fields
|
||||
// to match the new source selected
|
||||
if (name === 'source' && type === 'change') {
|
||||
const newInputSourceObj = inputSourceObjs?.find(
|
||||
s => s.id === data.source,
|
||||
);
|
||||
if (newInputSourceObj != null) {
|
||||
// Save the selected source ID to localStorage
|
||||
setLastSelectedSourceId(newInputSourceObj.id);
|
||||
const watchedSource = useWatch({ control, name: 'source' });
|
||||
const prevSourceRef = useRef(watchedSource);
|
||||
|
||||
setValue(
|
||||
'select',
|
||||
newInputSourceObj?.defaultTableSelectExpression ?? '',
|
||||
);
|
||||
// Clear all search filters
|
||||
searchFilters.clearAllFilters();
|
||||
}
|
||||
useEffect(() => {
|
||||
// If the user changes the source dropdown, reset the select and orderby fields
|
||||
// to match the new source selected
|
||||
if (watchedSource !== prevSourceRef.current) {
|
||||
prevSourceRef.current = watchedSource;
|
||||
const newInputSourceObj = inputSourceObjs?.find(
|
||||
s => s.id === watchedSource,
|
||||
);
|
||||
if (newInputSourceObj != null) {
|
||||
// Save the selected source ID to localStorage
|
||||
setLastSelectedSourceId(newInputSourceObj.id);
|
||||
|
||||
setValue(
|
||||
'select',
|
||||
newInputSourceObj?.defaultTableSelectExpression ?? '',
|
||||
);
|
||||
// Clear all search filters
|
||||
searchFilters.clearAllFilters();
|
||||
}
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}
|
||||
}, [
|
||||
watch,
|
||||
inputSourceObj,
|
||||
watchedSource,
|
||||
setValue,
|
||||
inputSourceObjs,
|
||||
searchFilters,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import router from 'next/router';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { NativeSelect, NumberInput } from 'react-hook-form-mantine';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
|
@ -92,7 +92,7 @@ const AlertForm = ({
|
|||
}) => {
|
||||
const { data: source } = useSource({ id: sourceId });
|
||||
|
||||
const { control, handleSubmit, watch } = useForm<Alert>({
|
||||
const { control, handleSubmit } = useForm<Alert>({
|
||||
defaultValues: defaultValues || {
|
||||
interval: '5m',
|
||||
threshold: 1,
|
||||
|
|
@ -106,8 +106,13 @@ const AlertForm = ({
|
|||
resolver: zodResolver(SavedSearchAlertFormSchema),
|
||||
});
|
||||
|
||||
const groupBy = watch('groupBy');
|
||||
const thresholdType = watch('thresholdType');
|
||||
const groupBy = useWatch({ control, name: 'groupBy' });
|
||||
const thresholdType = useWatch({ control, name: 'thresholdType' });
|
||||
const channelType = useWatch({ control, name: 'channel.type' });
|
||||
const interval = useWatch({ control, name: 'interval' });
|
||||
const groupByValue = useWatch({ control, name: 'groupBy' });
|
||||
const threshold = useWatch({ control, name: 'threshold' });
|
||||
const thresholdTypeValue = useWatch({ control, name: 'thresholdType' });
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
|
|
@ -168,7 +173,7 @@ const AlertForm = ({
|
|||
<Text size="xxs" opacity={0.5} mb={4}>
|
||||
Send to
|
||||
</Text>
|
||||
<AlertChannelForm control={control} type={watch('channel.type')} />
|
||||
<AlertChannelForm control={control} type={channelType} />
|
||||
</Paper>
|
||||
{groupBy && thresholdType === AlertThresholdType.BELOW && (
|
||||
<MantineAlert
|
||||
|
|
@ -197,10 +202,10 @@ const AlertForm = ({
|
|||
where={where}
|
||||
whereLanguage={whereLanguage}
|
||||
select={select}
|
||||
interval={watch('interval')}
|
||||
groupBy={watch('groupBy')}
|
||||
threshold={watch('threshold')}
|
||||
thresholdType={watch('thresholdType')}
|
||||
interval={interval}
|
||||
groupBy={groupByValue}
|
||||
threshold={threshold}
|
||||
thresholdType={thresholdTypeValue}
|
||||
/>
|
||||
)}
|
||||
</Accordion.Panel>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { SourceKind } from '@hyperdx/common-utils/dist/types';
|
||||
import { Box, Group, Slider, Text } from '@mantine/core';
|
||||
|
||||
|
|
@ -67,17 +67,19 @@ function DBServiceMapPage() {
|
|||
) ?? defaultSource)
|
||||
: defaultSource;
|
||||
|
||||
const { control, watch } = useForm({
|
||||
const { control } = useForm({
|
||||
values: {
|
||||
source: source?.id,
|
||||
},
|
||||
});
|
||||
|
||||
watch((data, { name, type }) => {
|
||||
if (name === 'source' && type === 'change') {
|
||||
setSourceId(data.source ?? null);
|
||||
const watchedSource = useWatch({ control, name: 'source' });
|
||||
|
||||
useEffect(() => {
|
||||
if (watchedSource !== sourceId) {
|
||||
setSourceId(watchedSource ?? null);
|
||||
}
|
||||
});
|
||||
}, [watchedSource, sourceId, setSourceId]);
|
||||
|
||||
const [samplingFactor, setSamplingFactor] = useQueryState(
|
||||
'samplingFactor',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Controller, FieldError, useForm } from 'react-hook-form';
|
||||
import { Controller, FieldError, useForm, useWatch } from 'react-hook-form';
|
||||
import { TableConnection } from '@hyperdx/common-utils/dist/core/metadata';
|
||||
import {
|
||||
DashboardFilter,
|
||||
|
|
@ -94,7 +94,7 @@ const DashboardFilterEditForm = ({
|
|||
onClose,
|
||||
onCancel,
|
||||
}: DashboardFilterEditFormProps) => {
|
||||
const { handleSubmit, register, formState, control, watch, reset } =
|
||||
const { handleSubmit, register, formState, control, reset } =
|
||||
useForm<DashboardFilter>({
|
||||
defaultValues: filter,
|
||||
});
|
||||
|
|
@ -103,10 +103,10 @@ const DashboardFilterEditForm = ({
|
|||
reset(filter);
|
||||
}, [filter, reset]);
|
||||
|
||||
const sourceId = watch('source');
|
||||
const sourceId = useWatch({ control, name: 'source' });
|
||||
const { data: source } = useSource({ id: sourceId });
|
||||
|
||||
const metricType = watch('sourceMetricType');
|
||||
const metricType = useWatch({ control, name: 'sourceMetricType' });
|
||||
const tableName = source && getMetricTableName(source, metricType);
|
||||
const tableConnection: TableConnection | undefined = tableName
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export default function GranularityPicker({
|
|||
return (
|
||||
<Select
|
||||
disabled={disabled}
|
||||
data-testid="granularity-picker"
|
||||
data={[
|
||||
{
|
||||
value: 'auto' as const,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import Link from 'next/link';
|
|||
import cx from 'classnames';
|
||||
import sub from 'date-fns/sub';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
|
||||
import {
|
||||
Alert,
|
||||
|
|
@ -1036,13 +1036,20 @@ function KubernetesDashboardPage() {
|
|||
const logSource = sources?.find(s => s.id === logSourceId);
|
||||
const metricSource = sources?.find(s => s.id === metricSourceId);
|
||||
|
||||
const { control, watch } = useForm({
|
||||
const { control } = useForm({
|
||||
values: {
|
||||
logSourceId,
|
||||
metricSourceId,
|
||||
},
|
||||
});
|
||||
|
||||
const watchedLogSourceId = useWatch({ control, name: 'logSourceId' });
|
||||
const watchedMetricSourceId = useWatch({ control, name: 'metricSourceId' });
|
||||
|
||||
// Track previous values to detect user-initiated changes
|
||||
const prevLogSourceIdRef = useRef(logSourceId);
|
||||
const prevMetricSourceIdRef = useRef(metricSourceId);
|
||||
|
||||
useEffect(() => {
|
||||
if (logSourceId && logSourceId !== _logSourceId) {
|
||||
setLogSourceId(logSourceId);
|
||||
|
|
@ -1055,54 +1062,89 @@ function KubernetesDashboardPage() {
|
|||
}
|
||||
}, [metricSourceId, _metricSourceId, setMetricSourceId]);
|
||||
|
||||
watch((data, { name, type }) => {
|
||||
if (name === 'logSourceId' && type === 'change') {
|
||||
setLogSourceId(data.logSourceId ?? null);
|
||||
// Handle log source changes
|
||||
useEffect(() => {
|
||||
if (watchedLogSourceId === prevLogSourceIdRef.current) {
|
||||
return;
|
||||
}
|
||||
prevLogSourceIdRef.current = watchedLogSourceId;
|
||||
|
||||
// Default to the log source's correlated metric source
|
||||
if (data.logSourceId && sources) {
|
||||
const logSource = findSource(sources, { id: data.logSourceId });
|
||||
const correlatedMetricSource = logSource?.metricSourceId
|
||||
? findSource(sources, { id: logSource.metricSourceId })
|
||||
: undefined;
|
||||
if (
|
||||
correlatedMetricSource &&
|
||||
correlatedMetricSource.id !== data.metricSourceId
|
||||
) {
|
||||
setMetricSourceId(correlatedMetricSource.id);
|
||||
notifications.show({
|
||||
id: `${correlatedMetricSource.id}-auto-correlated-metric-source`,
|
||||
title: 'Updated Metrics Source',
|
||||
message: `Using correlated metrics source: ${correlatedMetricSource.name}`,
|
||||
});
|
||||
} else if (logSource && !correlatedMetricSource) {
|
||||
notifications.show({
|
||||
id: `${logSource.id}-not-correlated`,
|
||||
title: 'Warning',
|
||||
message: `The selected logs source is not correlated with a metrics source. Source correlations can be configured in Team Settings.`,
|
||||
color: 'yellow',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (name === 'metricSourceId' && type === 'change') {
|
||||
setMetricSourceId(data.metricSourceId ?? null);
|
||||
const metricSource = data.metricSourceId
|
||||
? findSource(sources, { id: data.metricSourceId })
|
||||
setLogSourceId(watchedLogSourceId ?? null);
|
||||
|
||||
// Default to the log source's correlated metric source
|
||||
if (watchedLogSourceId && sources) {
|
||||
const logSource = findSource(sources, { id: watchedLogSourceId });
|
||||
const correlatedMetricSource = logSource?.metricSourceId
|
||||
? findSource(sources, { id: logSource.metricSourceId })
|
||||
: undefined;
|
||||
if (
|
||||
metricSource &&
|
||||
data.logSourceId &&
|
||||
metricSource.logSourceId !== data.logSourceId
|
||||
correlatedMetricSource &&
|
||||
correlatedMetricSource.id !== watchedMetricSourceId
|
||||
) {
|
||||
setMetricSourceId(correlatedMetricSource.id);
|
||||
notifications.show({
|
||||
id: `${metricSource.id}-not-correlated`,
|
||||
id: `${correlatedMetricSource.id}-auto-correlated-metric-source`,
|
||||
title: 'Updated Metrics Source',
|
||||
message: `Using correlated metrics source: ${correlatedMetricSource.name}`,
|
||||
});
|
||||
} else if (logSource && !correlatedMetricSource) {
|
||||
notifications.show({
|
||||
id: `${logSource.id}-not-correlated`,
|
||||
title: 'Warning',
|
||||
message: `The selected metrics source is not correlated with the selected logs source. Source correlations can be configured in Team Settings.`,
|
||||
message: `The selected logs source is not correlated with a metrics source. Source correlations can be configured in Team Settings.`,
|
||||
color: 'yellow',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [
|
||||
watchedLogSourceId,
|
||||
watchedMetricSourceId,
|
||||
sources,
|
||||
setLogSourceId,
|
||||
setMetricSourceId,
|
||||
]);
|
||||
|
||||
// Handle metric source changes
|
||||
useEffect(() => {
|
||||
if (watchedMetricSourceId === prevMetricSourceIdRef.current) {
|
||||
return;
|
||||
}
|
||||
prevMetricSourceIdRef.current = watchedMetricSourceId;
|
||||
|
||||
setMetricSourceId(watchedMetricSourceId ?? null);
|
||||
|
||||
// Default to the metric source's correlated log source
|
||||
if (watchedMetricSourceId && sources) {
|
||||
const metricSource = findSource(sources, { id: watchedMetricSourceId });
|
||||
const correlatedLogSource = metricSource?.logSourceId
|
||||
? findSource(sources, { id: metricSource.logSourceId })
|
||||
: undefined;
|
||||
if (
|
||||
correlatedLogSource &&
|
||||
correlatedLogSource.id !== watchedLogSourceId
|
||||
) {
|
||||
setLogSourceId(correlatedLogSource.id);
|
||||
notifications.show({
|
||||
id: `${correlatedLogSource.id}-auto-correlated-log-source`,
|
||||
title: 'Updated Logs Source',
|
||||
message: `Using correlated logs source: ${correlatedLogSource.name}`,
|
||||
});
|
||||
} else if (metricSource && !correlatedLogSource) {
|
||||
notifications.show({
|
||||
id: `${metricSource.id}-not-correlated`,
|
||||
title: 'Warning',
|
||||
message: `The selected metrics source is not correlated with a logs source. Source correlations can be configured in Team Settings.`,
|
||||
color: 'yellow',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
watchedMetricSourceId,
|
||||
watchedLogSourceId,
|
||||
sources,
|
||||
setMetricSourceId,
|
||||
setLogSourceId,
|
||||
]);
|
||||
|
||||
const [activeTab, setActiveTab] = useQueryState('tab', {
|
||||
defaultValue: 'pods',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
useQueryState,
|
||||
useQueryStates,
|
||||
} from 'nuqs';
|
||||
import { UseControllerProps, useForm } from 'react-hook-form';
|
||||
import { UseControllerProps, useForm, useWatch } from 'react-hook-form';
|
||||
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
|
||||
import { DEFAULT_AUTO_GRANULARITY_MAX_BUCKETS } from '@hyperdx/common-utils/dist/core/renderChartConfig';
|
||||
import {
|
||||
|
|
@ -1421,7 +1421,7 @@ function ServicesDashboardPage() {
|
|||
};
|
||||
}, [appliedConfigParams, sources]);
|
||||
|
||||
const { control, watch, setValue, handleSubmit } = useForm({
|
||||
const { control, setValue, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
where: '',
|
||||
whereLanguage: 'sql' as 'sql' | 'lucene',
|
||||
|
|
@ -1430,10 +1430,10 @@ function ServicesDashboardPage() {
|
|||
},
|
||||
});
|
||||
|
||||
const service = watch('service');
|
||||
const sourceId = watch('source');
|
||||
const service = useWatch({ control, name: 'service' });
|
||||
const sourceId = useWatch({ control, name: 'source' });
|
||||
const { data: source } = useSource({
|
||||
id: watch('source'),
|
||||
id: sourceId,
|
||||
});
|
||||
|
||||
const [showFiltersModal, setShowFiltersModal] = useState(false);
|
||||
|
|
@ -1501,32 +1501,17 @@ function ServicesDashboardPage() {
|
|||
|
||||
// Auto-submit when source changes
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = watch((data, { name, type }) => {
|
||||
if (
|
||||
name === 'source' &&
|
||||
type === 'change' &&
|
||||
data.source &&
|
||||
data.source !== appliedConfig.source
|
||||
) {
|
||||
onSubmit();
|
||||
}
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, [appliedConfig.source, onSubmit, watch]);
|
||||
if (sourceId && sourceId !== appliedConfig.source) {
|
||||
onSubmit();
|
||||
}
|
||||
}, [sourceId, appliedConfig.source, onSubmit]);
|
||||
|
||||
// Auto-submit when service changes
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = watch((data, { name, type }) => {
|
||||
if (
|
||||
name === 'service' &&
|
||||
type === 'change' &&
|
||||
data.service !== appliedConfig.service
|
||||
) {
|
||||
onSubmit();
|
||||
}
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, [appliedConfig.service, onSubmit, watch]);
|
||||
if (service !== appliedConfig.service) {
|
||||
onSubmit();
|
||||
}
|
||||
}, [service, appliedConfig.service, onSubmit]);
|
||||
|
||||
return (
|
||||
<Box p="sm">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
useQueryState,
|
||||
useQueryStates,
|
||||
} from 'nuqs';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { NumberParam } from 'serialize-query-params';
|
||||
import {
|
||||
StringParam,
|
||||
|
|
@ -245,7 +245,7 @@ const appliedConfigMap = {
|
|||
export default function SessionsPage() {
|
||||
const [appliedConfig, setAppliedConfig] = useQueryStates(appliedConfigMap);
|
||||
|
||||
const { control, watch, setValue, handleSubmit } = useForm({
|
||||
const { control, setValue, handleSubmit } = useForm({
|
||||
values: {
|
||||
where: appliedConfig.where,
|
||||
whereLanguage: appliedConfig.whereLanguage,
|
||||
|
|
@ -253,11 +253,11 @@ export default function SessionsPage() {
|
|||
},
|
||||
});
|
||||
|
||||
const where = watch('where');
|
||||
const whereLanguage = watch('whereLanguage');
|
||||
const sourceId = watch('source');
|
||||
const where = useWatch({ control, name: 'where' });
|
||||
const whereLanguage = useWatch({ control, name: 'whereLanguage' });
|
||||
const sourceId = useWatch({ control, name: 'source' });
|
||||
const { data: sessionSource, isPending: isSessionSourceLoading } = useSource({
|
||||
id: watch('source'),
|
||||
id: sourceId,
|
||||
});
|
||||
|
||||
const { data: traceTrace } = useSource({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Fragment, useCallback, useMemo, useState } from 'react';
|
|||
import Head from 'next/head';
|
||||
import { HTTPError } from 'ky';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||
import { DEFAULT_METADATA_MAX_ROWS_TO_READ } from '@hyperdx/common-utils/dist/core/metadata';
|
||||
import {
|
||||
SourceKind,
|
||||
|
|
@ -604,7 +604,7 @@ function ClickhouseSettingForm({
|
|||
<SelectControlled
|
||||
control={form.control}
|
||||
name="value"
|
||||
value={form.watch('value')}
|
||||
value={useWatch({ control: form.control, name: 'value' })}
|
||||
data={[displayValue(true), displayValue(false)]}
|
||||
size="xs"
|
||||
placeholder="Please select"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useCallback, useContext, useMemo, useState } from 'react';
|
|||
import { sq } from 'date-fns/locale';
|
||||
import ms from 'ms';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
|
||||
import {
|
||||
ChartConfigWithDateRange,
|
||||
|
|
@ -82,14 +82,14 @@ export default function ContextSubpanel({
|
|||
dbSqlRowTableConfig ?? {};
|
||||
const [range, setRange] = useState<number>(ms('30s'));
|
||||
const [contextBy, setContextBy] = useState<ContextBy>(ContextBy.All);
|
||||
const { control, watch } = useForm({
|
||||
const { control } = useForm({
|
||||
defaultValues: {
|
||||
where: '',
|
||||
whereLanguage: originalLanguage ?? ('lucene' as 'lucene' | 'sql'),
|
||||
},
|
||||
});
|
||||
|
||||
const formWhere = watch('where');
|
||||
const formWhere = useWatch({ control, name: 'where' });
|
||||
const [debouncedWhere] = useDebouncedValue(formWhere, 1000);
|
||||
|
||||
// State management for nested panels
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
Control,
|
||||
|
|
@ -459,7 +459,7 @@ export default function EditTimeChartForm({
|
|||
[chartConfig],
|
||||
);
|
||||
|
||||
const { control, watch, setValue, handleSubmit, register } =
|
||||
const { control, setValue, handleSubmit, register } =
|
||||
useForm<SavedChartConfigWithSeries>({
|
||||
defaultValues: configWithSeries,
|
||||
values: configWithSeries,
|
||||
|
|
@ -478,13 +478,21 @@ export default function EditTimeChartForm({
|
|||
|
||||
const [isSampleEventsOpen, setIsSampleEventsOpen] = useState(false);
|
||||
|
||||
const select = watch('select');
|
||||
const sourceId = watch('source');
|
||||
const whereLanguage = watch('whereLanguage');
|
||||
const alert = watch('alert');
|
||||
const seriesReturnType = watch('seriesReturnType');
|
||||
const compareToPreviousPeriod = watch('compareToPreviousPeriod');
|
||||
const groupBy = watch('groupBy');
|
||||
const select = useWatch({ control, name: 'select' });
|
||||
const sourceId = useWatch({ control, name: 'source' });
|
||||
const whereLanguage = useWatch({ control, name: 'whereLanguage' });
|
||||
const alert = useWatch({ control, name: 'alert' });
|
||||
const seriesReturnType = useWatch({ control, name: 'seriesReturnType' });
|
||||
const compareToPreviousPeriod = useWatch({
|
||||
control,
|
||||
name: 'compareToPreviousPeriod',
|
||||
});
|
||||
const groupBy = useWatch({ control, name: 'groupBy' });
|
||||
const displayType =
|
||||
useWatch({ control, name: 'displayType' }) ?? DisplayType.Line;
|
||||
const markdown = useWatch({ control, name: 'markdown' });
|
||||
const alertChannelType = useWatch({ control, name: 'alert.channel.type' });
|
||||
const granularity = useWatch({ control, name: 'granularity' });
|
||||
|
||||
const { data: tableSource } = useSource({ id: sourceId });
|
||||
const databaseName = tableSource?.from.databaseName;
|
||||
|
|
@ -493,8 +501,6 @@ export default function EditTimeChartForm({
|
|||
// const tableSource = tableSourceWatch();
|
||||
// const databaseName = tableSourceWatch('from.databaseName');
|
||||
// const tableName = tableSourceWatch('from.tableName');
|
||||
|
||||
const displayType = watch('displayType') ?? DisplayType.Line;
|
||||
const activeTab = useMemo(() => {
|
||||
switch (displayType) {
|
||||
case DisplayType.Search:
|
||||
|
|
@ -636,17 +642,27 @@ export default function EditTimeChartForm({
|
|||
[onSave, displayType],
|
||||
);
|
||||
|
||||
watch((_, { name, type }) => {
|
||||
// Track previous values for detecting changes
|
||||
const prevGranularityRef = useRef(granularity);
|
||||
const prevDisplayTypeRef = useRef(displayType);
|
||||
|
||||
useEffect(() => {
|
||||
// Emulate the granularity picker auto-searching similar to dashboards
|
||||
if (name === 'granularity' && type === 'change') {
|
||||
if (granularity !== prevGranularityRef.current) {
|
||||
prevGranularityRef.current = granularity;
|
||||
onSubmit();
|
||||
}
|
||||
if (name === 'displayType' && type === 'change') {
|
||||
if (_.displayType === DisplayType.Search && typeof select !== 'string') {
|
||||
}, [granularity, onSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (displayType !== prevDisplayTypeRef.current) {
|
||||
prevDisplayTypeRef.current = displayType;
|
||||
|
||||
if (displayType === DisplayType.Search && typeof select !== 'string') {
|
||||
setValue('select', '');
|
||||
setValue('series', []);
|
||||
}
|
||||
if (_.displayType !== DisplayType.Search && typeof select === 'string') {
|
||||
if (displayType !== DisplayType.Search && typeof select === 'string') {
|
||||
const defaultSeries: SavedChartConfigWithSelectArray['select'] = [
|
||||
{
|
||||
aggFn: 'count',
|
||||
|
|
@ -661,7 +677,7 @@ export default function EditTimeChartForm({
|
|||
}
|
||||
onSubmit();
|
||||
}
|
||||
});
|
||||
}, [displayType, select, setValue, onSubmit]);
|
||||
|
||||
// Emulate the date range picker auto-searching similar to dashboards
|
||||
useEffect(() => {
|
||||
|
|
@ -821,7 +837,7 @@ export default function EditTimeChartForm({
|
|||
<Box p="md" mb="md">
|
||||
<HDXMarkdownChart
|
||||
config={{
|
||||
markdown: watch('markdown') || 'Preview',
|
||||
markdown: markdown || 'Preview',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
|
@ -1059,7 +1075,7 @@ export default function EditTimeChartForm({
|
|||
</Text>
|
||||
<AlertChannelForm
|
||||
control={control}
|
||||
type={watch('alert.channel.type')}
|
||||
type={alertChannelType}
|
||||
namePrefix="alert."
|
||||
/>
|
||||
</Paper>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { parseAsJson, useQueryState } from 'nuqs';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
|
||||
import { SourceKind } from '@hyperdx/common-utils/dist/types';
|
||||
import {
|
||||
|
|
@ -53,15 +53,17 @@ export default function DBTracePanel({
|
|||
};
|
||||
'data-testid'?: string;
|
||||
}) {
|
||||
const { control, watch } = useForm({
|
||||
const { control } = useForm({
|
||||
defaultValues: {
|
||||
source: childSourceId,
|
||||
},
|
||||
});
|
||||
|
||||
const sourceId = useWatch({ control, name: 'source' });
|
||||
|
||||
const { data: childSourceData, isLoading: isChildSourceDataLoading } =
|
||||
useSource({
|
||||
id: watch('source'),
|
||||
id: sourceId,
|
||||
});
|
||||
|
||||
const { data: parentSourceData, isLoading: isParentSourceDataLoading } =
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
|
||||
import {
|
||||
ChartConfigWithDateRange,
|
||||
|
|
@ -84,25 +84,29 @@ export const KubernetesFilters: React.FC<KubernetesFiltersProps> = ({
|
|||
const [namespaceName, setNamespaceName] = useState<string | null>(null);
|
||||
const [clusterName, setClusterName] = useState<string | null>(null);
|
||||
|
||||
const { control, watch, setValue } = useForm({
|
||||
const { control, 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]);
|
||||
const watchedSearchQuery = useWatch({ control, name: 'searchQuery' });
|
||||
const prevSearchQueryRef = useRef(searchQuery);
|
||||
|
||||
// Update search form value when search query changes
|
||||
// Sync form changes to parent state
|
||||
useEffect(() => {
|
||||
setValue('searchQuery', searchQuery);
|
||||
if (watchedSearchQuery !== prevSearchQueryRef.current) {
|
||||
prevSearchQueryRef.current = watchedSearchQuery ?? '';
|
||||
setSearchQuery(watchedSearchQuery ?? '');
|
||||
}
|
||||
}, [watchedSearchQuery, setSearchQuery]);
|
||||
|
||||
// Update search form value when search query changes from parent
|
||||
useEffect(() => {
|
||||
if (searchQuery !== prevSearchQueryRef.current) {
|
||||
prevSearchQueryRef.current = searchQuery;
|
||||
setValue('searchQuery', searchQuery);
|
||||
}
|
||||
}, [searchQuery, setValue]);
|
||||
|
||||
// Helper function to extract value from search query
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import {
|
||||
Button,
|
||||
Checkbox as MCheckbox,
|
||||
|
|
@ -44,7 +44,7 @@ export const NumberFormatForm: React.FC<{
|
|||
onApply: (value: NumberFormat) => void;
|
||||
onClose: () => void;
|
||||
}> = ({ value, onApply, onClose }) => {
|
||||
const { register, handleSubmit, watch, setValue } = useForm<NumberFormat>({
|
||||
const { register, handleSubmit, control, setValue } = useForm<NumberFormat>({
|
||||
values: value,
|
||||
defaultValues: {
|
||||
factor: 1,
|
||||
|
|
@ -56,7 +56,15 @@ export const NumberFormatForm: React.FC<{
|
|||
},
|
||||
});
|
||||
|
||||
const values = watch();
|
||||
const values = useWatch({ control });
|
||||
const valuesWithDefaults = values ?? {
|
||||
factor: 1,
|
||||
output: 'number' as const,
|
||||
mantissa: 2,
|
||||
thousandSeparated: true,
|
||||
average: false,
|
||||
decimalBytes: false,
|
||||
};
|
||||
|
||||
const testNumber = 1234;
|
||||
|
||||
|
|
@ -91,7 +99,10 @@ export const NumberFormatForm: React.FC<{
|
|||
>
|
||||
<NativeSelect
|
||||
label="Output format"
|
||||
leftSection={values.output && FORMAT_ICONS[values.output]}
|
||||
leftSection={
|
||||
valuesWithDefaults.output &&
|
||||
FORMAT_ICONS[valuesWithDefaults.output]
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
data={[
|
||||
{ value: 'number', label: 'Number' },
|
||||
|
|
@ -102,7 +113,7 @@ export const NumberFormatForm: React.FC<{
|
|||
]}
|
||||
{...register('output')}
|
||||
/>
|
||||
{values.output === 'currency' && (
|
||||
{valuesWithDefaults.output === 'currency' && (
|
||||
<TextInput
|
||||
w={80}
|
||||
label="Symbol"
|
||||
|
|
@ -127,11 +138,11 @@ export const NumberFormatForm: React.FC<{
|
|||
>
|
||||
Example
|
||||
</div>
|
||||
{formatNumber(testNumber || 0, values)}
|
||||
{formatNumber(testNumber || 0, valuesWithDefaults as NumberFormat)}
|
||||
</Paper>
|
||||
</div>
|
||||
|
||||
{values.output !== 'time' && (
|
||||
{valuesWithDefaults.output !== 'time' && (
|
||||
<div>
|
||||
<div className="fs-8 mt-2 fw-bold mb-1">Decimals</div>
|
||||
<Slider
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import { SavedChartConfig } from '@hyperdx/common-utils/dist/types';
|
||||
import {
|
||||
Box,
|
||||
|
|
@ -46,7 +46,7 @@ export default function SaveToDashboardModal({
|
|||
const createDashboard = useCreateDashboard();
|
||||
const updateDashboard = useUpdateDashboard();
|
||||
|
||||
const { control, handleSubmit, reset, watch, formState } = useForm<{
|
||||
const { control, handleSubmit, reset, formState } = useForm<{
|
||||
dashboardId: string;
|
||||
newDashboardName: string;
|
||||
}>({
|
||||
|
|
@ -56,7 +56,7 @@ export default function SaveToDashboardModal({
|
|||
},
|
||||
});
|
||||
|
||||
const dashboardId = watch('dashboardId');
|
||||
const dashboardId = useWatch({ control, name: 'dashboardId' });
|
||||
const isCreatingNew = dashboardId === CREATE_NEW_DASHBOARD_VALUE;
|
||||
|
||||
// Reset form when modal is closed
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
useFieldArray,
|
||||
useForm,
|
||||
UseFormSetValue,
|
||||
UseFormWatch,
|
||||
useWatch,
|
||||
} from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
|
@ -178,20 +177,23 @@ function FormRow({
|
|||
|
||||
function HighlightedAttributeExpressionsFormRow({
|
||||
control,
|
||||
watch,
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
}: TableModelProps & {
|
||||
}: Omit<TableModelProps, 'setValue'> & {
|
||||
name:
|
||||
| 'highlightedTraceAttributeExpressions'
|
||||
| 'highlightedRowAttributeExpressions';
|
||||
label: string;
|
||||
helpText?: string;
|
||||
}) {
|
||||
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
|
||||
const tableName = watch(`from.tableName`);
|
||||
const connectionId = watch(`connection`);
|
||||
const databaseName = useWatch({
|
||||
control,
|
||||
name: 'from.databaseName',
|
||||
defaultValue: DEFAULT_DATABASE,
|
||||
});
|
||||
const tableName = useWatch({ control, name: 'from.tableName' });
|
||||
const connectionId = useWatch({ control, name: 'connection' });
|
||||
|
||||
const {
|
||||
fields: highlightedAttributes,
|
||||
|
|
@ -286,11 +288,7 @@ function HighlightedAttributeExpressionsFormRow({
|
|||
}
|
||||
|
||||
/** Component for configuring one or more materialized views */
|
||||
function MaterializedViewsFormSection({
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
}: TableModelProps) {
|
||||
function MaterializedViewsFormSection({ control, setValue }: TableModelProps) {
|
||||
const databaseName =
|
||||
useWatch({ control, name: `from.databaseName` }) || DEFAULT_DATABASE;
|
||||
|
||||
|
|
@ -320,7 +318,6 @@ function MaterializedViewsFormSection({
|
|||
{materializedViews.map((field, index) => (
|
||||
<MaterializedViewFormSection
|
||||
key={field.id}
|
||||
watch={watch}
|
||||
control={control}
|
||||
mvIndex={index}
|
||||
setValue={setValue}
|
||||
|
|
@ -354,7 +351,6 @@ function MaterializedViewsFormSection({
|
|||
|
||||
/** Component for configuring a single materialized view */
|
||||
function MaterializedViewFormSection({
|
||||
watch,
|
||||
control,
|
||||
mvIndex,
|
||||
onRemove,
|
||||
|
|
@ -471,7 +467,6 @@ function MaterializedViewFormSection({
|
|||
<AggregatedColumnsFormSection
|
||||
control={control}
|
||||
mvIndex={mvIndex}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Divider />
|
||||
|
|
@ -482,7 +477,6 @@ function MaterializedViewFormSection({
|
|||
/** Component for configuring the Aggregated Columns list for a single materialized view */
|
||||
function AggregatedColumnsFormSection({
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
mvIndex,
|
||||
}: TableModelProps & { mvIndex: number }) {
|
||||
|
|
@ -500,58 +494,78 @@ function AggregatedColumnsFormSection({
|
|||
appendAggregate({ sourceColumn: '', aggFn: 'avg', mvColumn: '' });
|
||||
}, [appendAggregate]);
|
||||
|
||||
const kind = useWatch({ control, name: 'kind' });
|
||||
const connection = useWatch({ control, name: 'connection' });
|
||||
const mvTableName = useWatch({
|
||||
control,
|
||||
name: `materializedViews.${mvIndex}.tableName`,
|
||||
});
|
||||
const mvDatabaseName = useWatch({
|
||||
control,
|
||||
name: `materializedViews.${mvIndex}.databaseName`,
|
||||
});
|
||||
const fromDatabaseName = useWatch({ control, name: 'from.databaseName' });
|
||||
const fromTableName = useWatch({ control, name: 'from.tableName' });
|
||||
const prevMvTableNameRef = useRef(mvTableName);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = watch(async (value, { name, type }) => {
|
||||
(async () => {
|
||||
try {
|
||||
if (
|
||||
(value.kind === SourceKind.Log || value.kind === SourceKind.Trace) &&
|
||||
value.connection &&
|
||||
value.materializedViews?.[mvIndex] &&
|
||||
value.materializedViews[mvIndex].databaseName &&
|
||||
value.materializedViews[mvIndex].tableName &&
|
||||
value.from?.databaseName &&
|
||||
value.from?.tableName &&
|
||||
name === `materializedViews.${mvIndex}.tableName` &&
|
||||
type === 'change'
|
||||
) {
|
||||
const mvDatabaseName = value.materializedViews[mvIndex].databaseName;
|
||||
const mvTableName = value.materializedViews[mvIndex].tableName;
|
||||
if (mvTableName !== prevMvTableNameRef.current) {
|
||||
prevMvTableNameRef.current = mvTableName;
|
||||
|
||||
const config = await inferMaterializedViewConfig(
|
||||
{
|
||||
databaseName: mvDatabaseName,
|
||||
tableName: mvTableName,
|
||||
connectionId: value.connection,
|
||||
},
|
||||
{
|
||||
databaseName: value.from.databaseName,
|
||||
tableName: value.from.tableName,
|
||||
connectionId: value.connection,
|
||||
},
|
||||
);
|
||||
if (
|
||||
(kind === SourceKind.Log || kind === SourceKind.Trace) &&
|
||||
connection &&
|
||||
mvDatabaseName &&
|
||||
mvTableName &&
|
||||
fromDatabaseName &&
|
||||
fromTableName
|
||||
) {
|
||||
const config = await inferMaterializedViewConfig(
|
||||
{
|
||||
databaseName: mvDatabaseName,
|
||||
tableName: mvTableName,
|
||||
connectionId: connection,
|
||||
},
|
||||
{
|
||||
databaseName: fromDatabaseName,
|
||||
tableName: fromTableName,
|
||||
connectionId: connection,
|
||||
},
|
||||
);
|
||||
|
||||
if (config) {
|
||||
setValue(`materializedViews.${mvIndex}`, config);
|
||||
replaceAggregates(config.aggregatedColumns ?? []);
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
message:
|
||||
'Partially inferred materialized view configuration from view schema.',
|
||||
});
|
||||
} else {
|
||||
notifications.show({
|
||||
color: 'yellow',
|
||||
message: 'Unable to infer materialized view configuration.',
|
||||
});
|
||||
if (config) {
|
||||
setValue(`materializedViews.${mvIndex}`, config);
|
||||
replaceAggregates(config.aggregatedColumns ?? []);
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
message:
|
||||
'Partially inferred materialized view configuration from view schema.',
|
||||
});
|
||||
} else {
|
||||
notifications.show({
|
||||
color: 'yellow',
|
||||
message: 'Unable to infer materialized view configuration.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [watch, mvIndex, replaceAggregates, setValue]);
|
||||
})();
|
||||
}, [
|
||||
mvTableName,
|
||||
kind,
|
||||
connection,
|
||||
mvDatabaseName,
|
||||
fromDatabaseName,
|
||||
fromTableName,
|
||||
mvIndex,
|
||||
replaceAggregates,
|
||||
setValue,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
|
@ -571,7 +585,6 @@ function AggregatedColumnsFormSection({
|
|||
{aggregates.map((field, colIndex) => (
|
||||
<AggregatedColumnRow
|
||||
key={field.id}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
control={control}
|
||||
mvIndex={mvIndex}
|
||||
|
|
@ -676,10 +689,14 @@ function AggregatedColumnRow({
|
|||
// custom always points towards the url param
|
||||
|
||||
export function LogTableModelForm(props: TableModelProps) {
|
||||
const { control, watch } = props;
|
||||
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
|
||||
const tableName = watch(`from.tableName`);
|
||||
const connectionId = watch(`connection`);
|
||||
const { control } = props;
|
||||
const databaseName = useWatch({
|
||||
control,
|
||||
name: 'from.databaseName',
|
||||
defaultValue: DEFAULT_DATABASE,
|
||||
});
|
||||
const tableName = useWatch({ control, name: 'from.tableName' });
|
||||
const connectionId = useWatch({ control, name: 'connection' });
|
||||
|
||||
const [showOptionalFields, setShowOptionalFields] = useState(false);
|
||||
|
||||
|
|
@ -925,10 +942,14 @@ export function LogTableModelForm(props: TableModelProps) {
|
|||
}
|
||||
|
||||
export function TraceTableModelForm(props: TableModelProps) {
|
||||
const { control, watch } = props;
|
||||
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
|
||||
const tableName = watch(`from.tableName`);
|
||||
const connectionId = watch(`connection`);
|
||||
const { control } = props;
|
||||
const databaseName = useWatch({
|
||||
control,
|
||||
name: 'from.databaseName',
|
||||
defaultValue: DEFAULT_DATABASE,
|
||||
});
|
||||
const tableName = useWatch({ control, name: 'from.tableName' });
|
||||
const connectionId = useWatch({ control, name: 'connection' });
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
|
|
@ -1205,19 +1226,21 @@ export function TraceTableModelForm(props: TableModelProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export function SessionTableModelForm({
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
}: TableModelProps) {
|
||||
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
|
||||
const connectionId = watch(`connection`);
|
||||
export function SessionTableModelForm({ control, setValue }: TableModelProps) {
|
||||
const databaseName = useWatch({
|
||||
control,
|
||||
name: 'from.databaseName',
|
||||
defaultValue: DEFAULT_DATABASE,
|
||||
});
|
||||
const connectionId = useWatch({ control, name: 'connection' });
|
||||
const tableName = useWatch({ control, name: 'from.tableName' });
|
||||
const prevTableNameRef = useRef(tableName);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = watch(async (value, { name, type }) => {
|
||||
(async () => {
|
||||
try {
|
||||
const tableName = value.from?.tableName;
|
||||
if (tableName && name === 'from.tableName' && type === 'change') {
|
||||
if (tableName && tableName !== prevTableNameRef.current) {
|
||||
prevTableNameRef.current = tableName;
|
||||
const isValid = await isValidSessionsTable({
|
||||
databaseName,
|
||||
tableName,
|
||||
|
|
@ -1238,10 +1261,8 @@ export function SessionTableModelForm({
|
|||
message: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [setValue, watch, databaseName, connectionId]);
|
||||
})();
|
||||
}, [tableName, databaseName, connectionId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -1259,48 +1280,55 @@ export function SessionTableModelForm({
|
|||
|
||||
interface TableModelProps {
|
||||
control: Control<TSourceUnion>;
|
||||
watch: UseFormWatch<TSourceUnion>;
|
||||
setValue: UseFormSetValue<TSourceUnion>;
|
||||
}
|
||||
|
||||
export function MetricTableModelForm({
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
}: TableModelProps) {
|
||||
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
|
||||
const connectionId = watch(`connection`);
|
||||
export function MetricTableModelForm({ control, setValue }: TableModelProps) {
|
||||
const databaseName = useWatch({
|
||||
control,
|
||||
name: 'from.databaseName',
|
||||
defaultValue: DEFAULT_DATABASE,
|
||||
});
|
||||
const connectionId = useWatch({ control, name: 'connection' });
|
||||
const metricTables = useWatch({ control, name: 'metricTables' });
|
||||
const prevMetricTablesRef = useRef(metricTables);
|
||||
|
||||
useEffect(() => {
|
||||
for (const [_key, _value] of Object.entries(OTEL_CLICKHOUSE_EXPRESSIONS)) {
|
||||
setValue(_key as any, _value);
|
||||
}
|
||||
const { unsubscribe } = watch(async (value, { name, type }) => {
|
||||
}, [setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (name && type === 'change') {
|
||||
const [prefix, suffix] = name.split('.');
|
||||
if (prefix === 'metricTables') {
|
||||
const tableName =
|
||||
value.kind === SourceKind.Metric
|
||||
? value?.metricTables?.[
|
||||
suffix as keyof typeof value.metricTables
|
||||
]
|
||||
: '';
|
||||
const metricType = suffix as MetricsDataType;
|
||||
const isValid = await isValidMetricTable({
|
||||
databaseName,
|
||||
tableName,
|
||||
connectionId,
|
||||
metricType,
|
||||
});
|
||||
if (!isValid) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
message: `${tableName} is not a valid OTEL ${metricType} schema.`,
|
||||
if (metricTables && prevMetricTablesRef.current) {
|
||||
// Check which metric table changed
|
||||
for (const metricType of Object.values(MetricsDataType)) {
|
||||
const newValue =
|
||||
metricTables[metricType as keyof typeof metricTables];
|
||||
const prevValue =
|
||||
prevMetricTablesRef.current[
|
||||
metricType as keyof typeof prevMetricTablesRef.current
|
||||
];
|
||||
|
||||
if (newValue !== prevValue) {
|
||||
const isValid = await isValidMetricTable({
|
||||
databaseName,
|
||||
tableName: newValue as string,
|
||||
connectionId,
|
||||
metricType: metricType as MetricsDataType,
|
||||
});
|
||||
if (!isValid) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
message: `${newValue} is not a valid OTEL ${metricType} schema.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prevMetricTablesRef.current = metricTables;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notifications.show({
|
||||
|
|
@ -1308,10 +1336,8 @@ export function MetricTableModelForm({
|
|||
message: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [setValue, watch, databaseName, connectionId]);
|
||||
})();
|
||||
}, [metricTables, databaseName, connectionId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -1348,48 +1374,22 @@ export function MetricTableModelForm({
|
|||
|
||||
function TableModelForm({
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
kind,
|
||||
}: {
|
||||
control: Control<TSourceUnion>;
|
||||
watch: UseFormWatch<TSourceUnion>;
|
||||
setValue: UseFormSetValue<TSourceUnion>;
|
||||
kind: SourceKind;
|
||||
}) {
|
||||
switch (kind) {
|
||||
case SourceKind.Log:
|
||||
return (
|
||||
<LogTableModelForm
|
||||
control={control}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
/>
|
||||
);
|
||||
return <LogTableModelForm control={control} setValue={setValue} />;
|
||||
case SourceKind.Trace:
|
||||
return (
|
||||
<TraceTableModelForm
|
||||
control={control}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
/>
|
||||
);
|
||||
return <TraceTableModelForm control={control} setValue={setValue} />;
|
||||
case SourceKind.Session:
|
||||
return (
|
||||
<SessionTableModelForm
|
||||
control={control}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
/>
|
||||
);
|
||||
return <SessionTableModelForm control={control} setValue={setValue} />;
|
||||
case SourceKind.Metric:
|
||||
return (
|
||||
<MetricTableModelForm
|
||||
control={control}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
/>
|
||||
);
|
||||
return <MetricTableModelForm control={control} setValue={setValue} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1412,7 +1412,6 @@ export function TableSourceForm({
|
|||
const { data: connections } = useConnections();
|
||||
|
||||
const {
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
formState,
|
||||
|
|
@ -1438,45 +1437,55 @@ export function TableSourceForm({
|
|||
},
|
||||
});
|
||||
|
||||
const watchedConnection = useWatch({ control, name: 'connection' });
|
||||
const watchedDatabaseName = useWatch({ control, name: 'from.databaseName' });
|
||||
const watchedTableName = useWatch({ control, name: 'from.tableName' });
|
||||
const watchedKind = useWatch({ control, name: 'kind' });
|
||||
const prevTableNameRef = useRef(watchedTableName);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = watch(async (_value, { name, type }) => {
|
||||
(async () => {
|
||||
try {
|
||||
// TODO: HDX-1768 get rid of this type assertion
|
||||
const value = _value as TSourceUnion;
|
||||
if (
|
||||
value.connection != null &&
|
||||
value.from?.databaseName != null &&
|
||||
(value.kind === SourceKind.Metric || value.from.tableName != null) &&
|
||||
name === 'from.tableName' &&
|
||||
type === 'change'
|
||||
) {
|
||||
const config = await inferTableSourceConfig({
|
||||
databaseName: value.from.databaseName,
|
||||
tableName:
|
||||
value.kind !== SourceKind.Metric ? value.from.tableName : '',
|
||||
connectionId: value.connection,
|
||||
});
|
||||
if (Object.keys(config).length > 0) {
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
message:
|
||||
'Automatically inferred source configuration from table schema.',
|
||||
if (watchedTableName !== prevTableNameRef.current) {
|
||||
prevTableNameRef.current = watchedTableName;
|
||||
|
||||
if (
|
||||
watchedConnection != null &&
|
||||
watchedDatabaseName != null &&
|
||||
(watchedKind === SourceKind.Metric || watchedTableName != null)
|
||||
) {
|
||||
const config = await inferTableSourceConfig({
|
||||
databaseName: watchedDatabaseName,
|
||||
tableName:
|
||||
watchedKind !== SourceKind.Metric ? watchedTableName : '',
|
||||
connectionId: watchedConnection,
|
||||
});
|
||||
if (Object.keys(config).length > 0) {
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
message:
|
||||
'Automatically inferred source configuration from table schema.',
|
||||
});
|
||||
}
|
||||
Object.entries(config).forEach(([key, value]) => {
|
||||
resetField(key as any, {
|
||||
keepDirty: true,
|
||||
defaultValue: value,
|
||||
});
|
||||
});
|
||||
}
|
||||
Object.entries(config).forEach(([key, value]) => {
|
||||
resetField(key as any, {
|
||||
keepDirty: true,
|
||||
defaultValue: value,
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [watch, resetField]);
|
||||
})();
|
||||
}, [
|
||||
watchedTableName,
|
||||
watchedConnection,
|
||||
watchedDatabaseName,
|
||||
watchedKind,
|
||||
resetField,
|
||||
]);
|
||||
|
||||
// Sets the default connection field to the first connection after the
|
||||
// connections have been loaded
|
||||
|
|
@ -1484,7 +1493,7 @@ export function TableSourceForm({
|
|||
resetField('connection', { defaultValue: connections?.[0]?.id });
|
||||
}, [connections, resetField]);
|
||||
|
||||
const kind: SourceKind = watch('kind');
|
||||
const kind: SourceKind = useWatch({ control, name: 'kind' });
|
||||
|
||||
const createSource = useCreateSource();
|
||||
const updateSource = useUpdateSource();
|
||||
|
|
@ -1492,59 +1501,119 @@ export function TableSourceForm({
|
|||
|
||||
// Bidirectional source linking
|
||||
const { data: sources } = useSources();
|
||||
const currentSourceId = watch('id');
|
||||
const currentSourceId = useWatch({ control, name: 'id' });
|
||||
|
||||
// Watch all potential correlation fields
|
||||
const logSourceId = useWatch({ control, name: 'logSourceId' });
|
||||
const traceSourceId = useWatch({ control, name: 'traceSourceId' });
|
||||
const metricSourceId = useWatch({ control, name: 'metricSourceId' });
|
||||
const sessionTraceSourceId = useWatch({ control, name: 'traceSourceId' }); // For sessions
|
||||
|
||||
const prevLogSourceIdRef = useRef(logSourceId);
|
||||
const prevTraceSourceIdRef = useRef(traceSourceId);
|
||||
const prevMetricSourceIdRef = useRef(metricSourceId);
|
||||
const prevSessionTraceSourceIdRef = useRef(sessionTraceSourceId);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = watch(async (_value, { name, type }) => {
|
||||
const value = _value as TSourceUnion;
|
||||
if (!currentSourceId || !sources || type !== 'change') return;
|
||||
(async () => {
|
||||
if (!currentSourceId || !sources || !kind) return;
|
||||
|
||||
const correlationFields = CORRELATION_FIELD_MAP[kind];
|
||||
if (!correlationFields || !name || !(name in correlationFields)) return;
|
||||
if (!correlationFields) return;
|
||||
|
||||
const fieldName = name as keyof TSourceUnion;
|
||||
const newTargetSourceId = value[fieldName] as string | undefined;
|
||||
const targetConfigs = correlationFields[fieldName];
|
||||
// Check each field for changes
|
||||
const changedFields: Array<{
|
||||
name: keyof TSourceUnion;
|
||||
value: string | undefined;
|
||||
}> = [];
|
||||
|
||||
for (const { targetKind, targetField } of targetConfigs) {
|
||||
// Find the previously linked source if any
|
||||
const previouslyLinkedSource = sources.find(
|
||||
s => s.kind === targetKind && s[targetField] === currentSourceId,
|
||||
);
|
||||
if (logSourceId !== prevLogSourceIdRef.current) {
|
||||
prevLogSourceIdRef.current = logSourceId;
|
||||
changedFields.push({
|
||||
name: 'logSourceId' as keyof TSourceUnion,
|
||||
value: logSourceId ?? undefined,
|
||||
});
|
||||
}
|
||||
if (traceSourceId !== prevTraceSourceIdRef.current) {
|
||||
prevTraceSourceIdRef.current = traceSourceId;
|
||||
changedFields.push({
|
||||
name: 'traceSourceId' as keyof TSourceUnion,
|
||||
value: traceSourceId ?? undefined,
|
||||
});
|
||||
}
|
||||
if (metricSourceId !== prevMetricSourceIdRef.current) {
|
||||
prevMetricSourceIdRef.current = metricSourceId;
|
||||
changedFields.push({
|
||||
name: 'metricSourceId' as keyof TSourceUnion,
|
||||
value: metricSourceId ?? undefined,
|
||||
});
|
||||
}
|
||||
if (
|
||||
sessionTraceSourceId !== prevSessionTraceSourceIdRef.current &&
|
||||
kind === SourceKind.Session
|
||||
) {
|
||||
prevSessionTraceSourceIdRef.current = sessionTraceSourceId;
|
||||
changedFields.push({
|
||||
name: 'traceSourceId' as keyof TSourceUnion,
|
||||
value: sessionTraceSourceId ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// If there was a previously linked source and it's different from the new one, unlink it
|
||||
if (
|
||||
previouslyLinkedSource &&
|
||||
previouslyLinkedSource.id !== newTargetSourceId
|
||||
) {
|
||||
await updateSource.mutateAsync({
|
||||
source: {
|
||||
...previouslyLinkedSource,
|
||||
[targetField]: undefined,
|
||||
} as TSource,
|
||||
});
|
||||
}
|
||||
for (const {
|
||||
name: fieldName,
|
||||
value: newTargetSourceId,
|
||||
} of changedFields) {
|
||||
if (!(fieldName in correlationFields)) continue;
|
||||
|
||||
// If a new source is selected, link it back
|
||||
if (newTargetSourceId) {
|
||||
const targetSource = sources.find(s => s.id === newTargetSourceId);
|
||||
if (targetSource && targetSource.kind === targetKind) {
|
||||
// Only update if the target field is empty to avoid overwriting existing correlations
|
||||
if (!targetSource[targetField]) {
|
||||
await updateSource.mutateAsync({
|
||||
source: {
|
||||
...targetSource,
|
||||
[targetField]: currentSourceId,
|
||||
} as TSource,
|
||||
});
|
||||
const targetConfigs = correlationFields[fieldName];
|
||||
|
||||
for (const { targetKind, targetField } of targetConfigs) {
|
||||
// Find the previously linked source if any
|
||||
const previouslyLinkedSource = sources.find(
|
||||
s => s.kind === targetKind && s[targetField] === currentSourceId,
|
||||
);
|
||||
|
||||
// If there was a previously linked source and it's different from the new one, unlink it
|
||||
if (
|
||||
previouslyLinkedSource &&
|
||||
previouslyLinkedSource.id !== newTargetSourceId
|
||||
) {
|
||||
await updateSource.mutateAsync({
|
||||
source: {
|
||||
...previouslyLinkedSource,
|
||||
[targetField]: undefined,
|
||||
} as TSource,
|
||||
});
|
||||
}
|
||||
|
||||
// If a new source is selected, link it back
|
||||
if (newTargetSourceId) {
|
||||
const targetSource = sources.find(s => s.id === newTargetSourceId);
|
||||
if (targetSource && targetSource.kind === targetKind) {
|
||||
// Only update if the target field is empty to avoid overwriting existing correlations
|
||||
if (!targetSource[targetField]) {
|
||||
await updateSource.mutateAsync({
|
||||
source: {
|
||||
...targetSource,
|
||||
[targetField]: currentSourceId,
|
||||
} as TSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [watch, kind, currentSourceId, sources, updateSource]);
|
||||
})();
|
||||
}, [
|
||||
logSourceId,
|
||||
traceSourceId,
|
||||
metricSourceId,
|
||||
sessionTraceSourceId,
|
||||
kind,
|
||||
currentSourceId,
|
||||
sources,
|
||||
updateSource,
|
||||
]);
|
||||
|
||||
const sourceFormSchema = sourceSchemaWithout({ id: true });
|
||||
const handleError = useCallback(
|
||||
|
|
@ -1679,8 +1748,12 @@ export function TableSourceForm({
|
|||
sourceFormSchema,
|
||||
]);
|
||||
|
||||
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
|
||||
const connectionId = watch(`connection`);
|
||||
const databaseName = useWatch({
|
||||
control,
|
||||
name: 'from.databaseName',
|
||||
defaultValue: DEFAULT_DATABASE,
|
||||
});
|
||||
const connectionId = useWatch({ control, name: 'connection' });
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -1780,12 +1853,7 @@ export function TableSourceForm({
|
|||
</FormRow>
|
||||
)}
|
||||
</Stack>
|
||||
<TableModelForm
|
||||
control={control}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
kind={kind}
|
||||
/>
|
||||
<TableModelForm control={control} setValue={setValue} kind={kind} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import { HTTPError } from 'ky';
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { Controller, SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||
import { ZodIssue } from 'zod';
|
||||
import { json, jsonParseLinter } from '@codemirror/lang-json';
|
||||
import { linter } from '@codemirror/lint';
|
||||
|
|
@ -270,7 +270,7 @@ export function WebhookForm({
|
|||
}
|
||||
};
|
||||
|
||||
const service = form.watch('service');
|
||||
const service = useWatch({ control: form.control, name: 'service' });
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export default function useRowWhere({
|
|||
return useCallback(
|
||||
(row: Record<string, any>) => {
|
||||
// Filter out synthetic columns that aren't in the database schema
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
const { __hyperdx_id, ...dbRow } = row;
|
||||
return processRowToWhereClause(dbRow, columnMap);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -192,4 +192,44 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
|
|||
await expect(dashboardTiles).toHaveCount(tileCountBefore - 1);
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'should update charts when granularity is changed',
|
||||
{ tag: '@dashboard' },
|
||||
async () => {
|
||||
await test.step('Create dashboard with a time series chart', async () => {
|
||||
await dashboardPage.createNewDashboard();
|
||||
|
||||
// Add a time series tile
|
||||
await dashboardPage.addTile();
|
||||
await dashboardPage.chartEditor.createBasicChart(
|
||||
'Time Series Test Chart',
|
||||
);
|
||||
|
||||
// Wait for chart to render
|
||||
const chartContainers = dashboardPage.getChartContainers();
|
||||
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('Change granularity and verify UI updates', async () => {
|
||||
// Find granularity dropdown (typically labeled "Granularity" or shows current value like "Auto")
|
||||
const granularityDropdown = dashboardPage.granularityPicker;
|
||||
await expect(granularityDropdown).toBeVisible();
|
||||
|
||||
// Get current value
|
||||
const currentValue = await granularityDropdown.inputValue();
|
||||
|
||||
// Change to a different granularity (e.g., "1m")
|
||||
await dashboardPage.changeGranularity('1 Minute Granularity');
|
||||
|
||||
// Verify the value changed
|
||||
const newValue = granularityDropdown;
|
||||
await expect(newValue).not.toHaveValue(currentValue);
|
||||
|
||||
// Verify chart is still visible (validates that the change worked)
|
||||
const chartContainers = dashboardPage.getChartContainers();
|
||||
await expect(chartContainers).toHaveCount(1);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export class DashboardPage {
|
|||
readonly page: Page;
|
||||
readonly timePicker: TimePickerComponent;
|
||||
readonly chartEditor: ChartEditorComponent;
|
||||
readonly granularityPicker: Locator;
|
||||
|
||||
private readonly createDashboardButton: Locator;
|
||||
private readonly addTileButton: Locator;
|
||||
|
|
@ -34,6 +35,7 @@ export class DashboardPage {
|
|||
);
|
||||
this.liveButton = page.locator('button:has-text("Live")');
|
||||
this.dashboardNameHeading = page.getByRole('heading', { level: 3 });
|
||||
this.granularityPicker = page.getByTestId('granularity-picker');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,6 +60,15 @@ export class DashboardPage {
|
|||
await this.page.waitForURL('**/dashboards**');
|
||||
}
|
||||
|
||||
async changeGranularity(granularity: string) {
|
||||
await this.granularityPicker.click();
|
||||
|
||||
// Wait for dropdown options to appear and click the desired option
|
||||
await this.page
|
||||
.locator('[role="option"]', { hasText: granularity })
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit dashboard name
|
||||
*/
|
||||
|
|
|
|||
41
yarn.lock
41
yarn.lock
|
|
@ -3975,6 +3975,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/compat@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@eslint/compat@npm:2.0.0"
|
||||
dependencies:
|
||||
"@eslint/core": "npm:^1.0.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.40 || 9
|
||||
peerDependenciesMeta:
|
||||
eslint:
|
||||
optional: true
|
||||
checksum: 10c0/ba848e8ea02fbfba9985d6a6e5aeedf5c44c4f57cb01fb6b9629d2ebfa37f9eafa4506ad9690fb805e0c395269d84e09eae46bfe41884bfff6171ebd68e17373
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/config-array@npm:^0.21.1":
|
||||
version: 0.21.1
|
||||
resolution: "@eslint/config-array@npm:0.21.1"
|
||||
|
|
@ -4004,6 +4018,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/core@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@eslint/core@npm:1.0.0"
|
||||
dependencies:
|
||||
"@types/json-schema": "npm:^7.0.15"
|
||||
checksum: 10c0/ce94edc4e7f78afac5815ba9afcae8c99d286a7d6f80efd596f6ba08dd1984277c4f3b2d6585f7a31d8235f9124ca311780ace664a7340a0cb19d9a71b413285
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/eslintrc@npm:^3.3.1":
|
||||
version: 3.3.3
|
||||
resolution: "@eslint/eslintrc@npm:3.3.3"
|
||||
|
|
@ -4294,6 +4317,7 @@ __metadata:
|
|||
"@codemirror/lang-json": "npm:^6.0.1"
|
||||
"@codemirror/lang-sql": "npm:^6.7.0"
|
||||
"@dagrejs/dagre": "npm:^1.1.5"
|
||||
"@eslint/compat": "npm:^2.0.0"
|
||||
"@hookform/devtools": "npm:^4.3.1"
|
||||
"@hookform/resolvers": "npm:^3.9.0"
|
||||
"@hyperdx/browser": "npm:^0.21.1"
|
||||
|
|
@ -4354,6 +4378,7 @@ __metadata:
|
|||
dayjs: "npm:^1.11.19"
|
||||
eslint-config-next: "npm:^16.0.10"
|
||||
eslint-plugin-playwright: "npm:^2.4.0"
|
||||
eslint-plugin-react-hook-form: "npm:^0.3.1"
|
||||
eslint-plugin-storybook: "npm:10.1.4"
|
||||
flat: "npm:^5.0.2"
|
||||
fuse.js: "npm:^6.6.2"
|
||||
|
|
@ -14599,6 +14624,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-react-hook-form@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "eslint-plugin-react-hook-form@npm:0.3.1"
|
||||
dependencies:
|
||||
requireindex: "npm:~1.1.0"
|
||||
checksum: 10c0/bb2abc506706b6d8c1419f7a2510d927f2193ecee59d851f73d3bb8b2aa9aabc8aa1f8481aae2af44dc1d43ed14845e2a18f03e9b8a37bd2250144257b81e47a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-react-hooks@npm:^7.0.0, eslint-plugin-react-hooks@npm:^7.0.1":
|
||||
version: 7.0.1
|
||||
resolution: "eslint-plugin-react-hooks@npm:7.0.1"
|
||||
|
|
@ -24584,6 +24618,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"requireindex@npm:~1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "requireindex@npm:1.1.0"
|
||||
checksum: 10c0/4d57e7c6d160c4d043a233a6a95a22c792ada72ff378c5ebd415be505b328f3c719f9188dd100f8fcaa8af513a85f39eee0a5047838d715e22e79544a3b7d002
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"requires-port@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "requires-port@npm:1.0.0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue