mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
parent
3019becb9f
commit
8584b4a454
5 changed files with 268 additions and 64 deletions
5
.changeset/tender-days-beam.md
Normal file
5
.changeset/tender-days-beam.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: source form was not loading properly for all sources
|
||||
|
|
@ -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)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue