fix: Fix default values in SourceForm (#1532)

Fixes: HDX-3116
This commit is contained in:
Tom Alexander 2025-12-29 16:29:05 -05:00 committed by GitHub
parent 3019becb9f
commit 8584b4a454
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 268 additions and 64 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
fix: source form was not loading properly for all sources

View file

@ -216,7 +216,7 @@ function SourceEditMenu({
</Menu.Item>
{IS_LOCAL_MODE ? (
<Menu.Item
data-testid="edit-source-menu-item"
data-testid="edit-sources-menu-item"
leftSection={<IconSettings size={14} />}
onClick={() => setModelFormExpanded(v => !v)}
>

View file

@ -289,8 +289,11 @@ function HighlightedAttributeExpressionsFormRow({
/** Component for configuring one or more materialized views */
function MaterializedViewsFormSection({ control, setValue }: TableModelProps) {
const databaseName =
useWatch({ control, name: `from.databaseName` }) || DEFAULT_DATABASE;
const databaseName = useWatch({
control,
name: `from.databaseName`,
defaultValue: DEFAULT_DATABASE,
});
const {
fields: materializedViews,
@ -357,13 +360,21 @@ function MaterializedViewFormSection({
setValue,
}: { mvIndex: number; onRemove: () => void } & TableModelProps) {
const connection = useWatch({ control, name: `connection` });
const sourceDatabaseName =
useWatch({ control, name: `from.databaseName` }) || DEFAULT_DATABASE;
const mvDatabaseName =
useWatch({ control, name: `materializedViews.${mvIndex}.databaseName` }) ||
sourceDatabaseName;
const mvTableName =
useWatch({ control, name: `materializedViews.${mvIndex}.tableName` }) || '';
const sourceDatabaseName = useWatch({
control,
name: `from.databaseName`,
defaultValue: DEFAULT_DATABASE,
});
const mvDatabaseName = useWatch({
control,
name: `materializedViews.${mvIndex}.databaseName`,
defaultValue: sourceDatabaseName,
});
const mvTableName = useWatch({
control,
name: `materializedViews.${mvIndex}.tableName`,
defaultValue: '',
});
return (
<Stack gap="sm">
@ -615,12 +626,17 @@ function AggregatedColumnRow({
onRemove: () => void;
}) {
const connectionId = useWatch({ control, name: `connection` });
const sourceDatabaseName =
useWatch({ control, name: `from.databaseName` }) || DEFAULT_DATABASE;
const sourceDatabaseName = useWatch({
control,
name: `from.databaseName`,
defaultValue: DEFAULT_DATABASE,
});
const sourceTableName = useWatch({ control, name: `from.tableName` });
const mvDatabaseName =
useWatch({ control, name: `materializedViews.${mvIndex}.databaseName` }) ||
sourceDatabaseName;
const mvDatabaseName = useWatch({
control,
name: `materializedViews.${mvIndex}.databaseName`,
defaultValue: sourceDatabaseName,
});
const mvTableName = useWatch({
control,
name: `materializedViews.${mvIndex}.tableName`,
@ -1226,7 +1242,7 @@ export function TraceTableModelForm(props: TableModelProps) {
);
}
export function SessionTableModelForm({ control, setValue }: TableModelProps) {
export function SessionTableModelForm({ control }: TableModelProps) {
const databaseName = useWatch({
control,
name: 'from.databaseName',
@ -1411,36 +1427,45 @@ export function TableSourceForm({
const { data: source } = useSource({ id: sourceId });
const { data: connections } = useConnections();
const {
control,
setValue,
formState,
handleSubmit,
resetField,
setError,
clearErrors,
} = useForm<TSourceUnion>({
defaultValues: {
kind: SourceKind.Log,
name: defaultName,
connection: connections?.[0]?.id,
from: {
databaseName: 'default',
tableName: '',
const { control, setValue, handleSubmit, resetField, setError, clearErrors } =
useForm<TSourceUnion>({
defaultValues: {
kind: SourceKind.Log,
name: defaultName,
connection: connections?.[0]?.id,
from: {
databaseName: 'default',
tableName: '',
},
},
},
// TODO: HDX-1768 remove type assertion
values: source as TSourceUnion,
resetOptions: {
keepDirtyValues: true,
keepErrors: true,
},
});
// TODO: HDX-1768 remove type assertion
values: source as TSourceUnion,
resetOptions: {
keepDirtyValues: true,
keepErrors: true,
},
});
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 watchedConnection = useWatch({
control,
name: 'connection',
defaultValue: source?.connection,
});
const watchedDatabaseName = useWatch({
control,
name: 'from.databaseName',
defaultValue: source?.from?.databaseName || DEFAULT_DATABASE,
});
const watchedTableName = useWatch({
control,
name: 'from.tableName',
defaultValue: source?.from?.tableName,
});
const watchedKind = useWatch({
control,
name: 'kind',
defaultValue: source?.kind || SourceKind.Log,
});
const prevTableNameRef = useRef(watchedTableName);
useEffect(() => {
@ -1493,7 +1518,11 @@ export function TableSourceForm({
resetField('connection', { defaultValue: connections?.[0]?.id });
}, [connections, resetField]);
const kind: SourceKind = useWatch({ control, name: 'kind' });
const kind = useWatch({
control,
name: 'kind',
defaultValue: source?.kind || SourceKind.Log,
});
const createSource = useCreateSource();
const updateSource = useUpdateSource();
@ -1751,9 +1780,13 @@ export function TableSourceForm({
const databaseName = useWatch({
control,
name: 'from.databaseName',
defaultValue: DEFAULT_DATABASE,
defaultValue: source?.from?.databaseName || DEFAULT_DATABASE,
});
const connectionId = useWatch({
control,
name: 'connection',
defaultValue: source?.connection,
});
const connectionId = useWatch({ control, name: 'connection' });
return (
<div

View file

@ -1,7 +1,79 @@
import { SearchPage } from '../page-objects/SearchPage';
import { expect, test } from '../utils/base-test';
test.describe('Sources Functionality', () => {
const COMMON_FIELDS = [
'Name',
'Source Data Type',
'Server Connection',
'Database',
'Table',
];
const LOG_FIELDS = [
...COMMON_FIELDS,
'Service Name Expression',
'Log Level Expression',
'Body Expression',
'Log Attributes Expression',
'Resource Attributes Expression',
'Displayed Timestamp Column',
'Correlated Metric Source',
'Correlated Trace Source',
'Trace Id Expression',
'Span Id Expression',
'Implicit Column Expression',
];
const TRACE_FIELDS = [
...COMMON_FIELDS,
'Duration Expression',
'Duration Precision',
'Trace Id Expression',
'Span Id Expression',
'Parent Span Id Expression',
'Span Name Expression',
'Span Kind Expression',
'Correlated Log Source',
'Correlated Session Source',
'Correlated Metric Source',
'Status Code Expression',
'Status Message Expression',
'Service Name Expression',
'Resource Attributes Expression',
'Event Attributes Expression',
'Span Events Expression',
'Implicit Column Expression',
'Displayed Timestamp Column',
];
const SESSION_FIELDS = [...COMMON_FIELDS, 'Correlated Trace Source'];
const METRIC_FIELDS = [
...COMMON_FIELDS.slice(0, -1), // Remove Table
'gauge Table',
'histogram Table',
'sum Table',
'summary Table',
'exponential histogram Table',
'Correlated Log Source',
];
const editableSourcesData = [
{ name: 'Demo Logs', fields: LOG_FIELDS, radioButtonName: 'Log' },
{ name: 'Demo Traces', fields: TRACE_FIELDS, radioButtonName: 'Trace' },
];
const allSourcesData = [
...editableSourcesData,
{
name: 'Demo Metrics',
fields: METRIC_FIELDS,
radioButtonName: 'OTEL Metrics',
},
{ name: 'Demo Sessions', fields: SESSION_FIELDS, radioButtonName: 'Session' },
];
test.describe('Sources Functionality', { tag: ['@sources'] }, () => {
let searchPage: SearchPage;
test.beforeEach(async ({ page }) => {
@ -11,21 +83,71 @@ test.describe('Sources Functionality', () => {
test('should open source settings menu', async () => {
// Click source settings menu
const sourceSettingsMenu = searchPage.page.locator(
'[data-testid="source-settings-menu"]',
);
await sourceSettingsMenu.click();
await searchPage.sourceMenu.click();
// Verify create new source menu item is visible
const createNewSourceMenuItem = searchPage.page.locator(
'[data-testid="create-new-source-menu-item"]',
);
await expect(createNewSourceMenuItem).toBeVisible();
await expect(searchPage.createNewSourceItem).toBeVisible();
// Verify edit source menu items are visible
const editSourceMenuItems = searchPage.page.locator(
'[data-testid="edit-source-menu-item"], [data-testid="edit-sources-menu-item"]',
);
await expect(editSourceMenuItems.first()).toBeVisible();
await expect(searchPage.editSourceMenuItem).toBeVisible();
});
test(
'should show the correct source form when modal is open',
{ tag: ['@sources'] },
async () => {
test.skip(
process.env.E2E_FULLSTACK === 'true',
'Skipping source form tests in fullstack mode due to UI differences',
);
for (const sourceData of editableSourcesData) {
await test.step(`Verify ${sourceData.name} fields`, async () => {
// Demo Logs is selected by default, so we don't need to select it again
if (sourceData.name !== 'Demo Logs') {
await searchPage.selectSource(sourceData.name);
}
await searchPage.openEditSourceModal();
await searchPage.sourceModalShowOptionalFields();
for (const field of sourceData.fields) {
await expect(
searchPage.page.getByText(field, { exact: true }),
).toBeVisible();
}
// press escape to close the modal
await searchPage.page.keyboard.press('Escape');
});
}
},
);
test('should show proper fields when creating a new source', async () => {
await searchPage.sourceMenu.click();
await searchPage.createNewSourceItem.click();
// for each source type (log, trace, session, metric), verify the correct fields are shown
for (const sourceData of allSourcesData) {
await test.step(`Verify ${sourceData.radioButtonName} source type`, async () => {
// Find the radio button by its label
const radioButton = searchPage.page.getByLabel(
sourceData.radioButtonName,
{ exact: true },
);
// Click the radio button
await radioButton.click();
// Show optional fields if the button exists
await searchPage.sourceModalShowOptionalFields();
// Verify fields
for (const field of sourceData.fields) {
await expect(
searchPage.page.getByText(field, { exact: true }),
).toBeVisible();
}
});
}
await searchPage.page.keyboard.press('Escape');
});
});

View file

@ -20,6 +20,8 @@ export class SearchPage {
readonly filters: FilterComponent;
readonly savedSearchModal: SavedSearchModalComponent;
readonly defaultTimeout: number = 3000;
readonly editSourceMenuItem: Locator;
// Page-specific locators
private readonly searchForm: Locator;
private readonly searchInput: Locator;
@ -27,6 +29,9 @@ export class SearchPage {
private readonly saveSearchButton: Locator;
private readonly luceneTab: Locator;
private readonly sqlTab: Locator;
private readonly sourceSelector: Locator;
private readonly sourceSettingsMenu: Locator;
private readonly createNewSourceMenuItem: Locator;
constructor(page: Page, defaultTimeout: number = 3000) {
this.page = page;
@ -43,12 +48,26 @@ export class SearchPage {
this.savedSearchModal = new SavedSearchModalComponent(page);
// Define page-specific locators
this.searchForm = page.locator('[data-testid="search-form"]');
this.searchInput = page.locator('[data-testid="search-input"]');
this.searchButton = page.locator('[data-testid="search-submit-button"]');
this.saveSearchButton = page.locator('[data-testid="save-search-button"]');
this.searchForm = page.getByTestId('search-form');
this.searchInput = page.getByTestId('search-input');
this.searchButton = page.getByTestId('search-submit-button');
this.saveSearchButton = page.getByTestId('save-search-button');
this.luceneTab = page.getByRole('button', { name: 'Lucene', exact: true });
this.sqlTab = page.getByRole('button', { name: 'SQL', exact: true });
this.sourceSelector = page.getByTestId('source-selector');
this.sourceSettingsMenu = page.getByTestId('source-settings-menu');
this.editSourceMenuItem = page.getByTestId('edit-sources-menu-item');
this.createNewSourceMenuItem = page.getByTestId(
'create-new-source-menu-item',
);
}
get sourceMenu() {
return this.sourceSettingsMenu;
}
get createNewSourceItem() {
return this.createNewSourceMenuItem;
}
/**
@ -60,6 +79,27 @@ export class SearchPage {
await this.table.waitForRowsToPopulate();
}
async selectSource(sourceName: string) {
await this.sourceSelector.click();
await this.page
.getByRole('option', { name: sourceName, exact: true })
.click();
}
async openEditSourceModal() {
await this.sourceSettingsMenu.click();
await this.editSourceMenuItem.click();
}
async sourceModalShowOptionalFields() {
const optionalFieldsButton = this.page.getByText(
'Configure Optional Fields',
);
if (await optionalFieldsButton.isVisible()) {
await optionalFieldsButton.click();
}
}
/**
* Perform a search with the given query
*/
@ -218,4 +258,8 @@ export class SearchPage {
get sqlModeTab() {
return this.sqlTab;
}
get sourceDropdown() {
return this.sourceSelector;
}
}