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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tender-days-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---

fix: source form was not loading properly for all sources
2 changes: 1 addition & 1 deletion packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
>
Expand Down
125 changes: 79 additions & 46 deletions packages/app/src/components/SourceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was the key fix (setting defaults for each of these)

});
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(() => {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
148 changes: 135 additions & 13 deletions packages/app/tests/e2e/features/sources.spec.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -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');
});
});
Loading
Loading