Correlations: Add query editor and target field to settings page (#55567)

* Fix: use type=button in editor

* Grafana-UI: TextArea: make ctextare a block element

* WIP: add field & target query to correlations

* add table query helpers & test ordering

* refactor some tests for disappearance

* chore: move QueryEditorField & add tests

* cleanup & fix typo

* revert textarea changes

* update form to support new config

* move defaults
This commit is contained in:
Giordano Ricci 2022-10-10 12:32:21 +01:00 committed by GitHub
parent db68fa358f
commit da9d8fe14f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 425 additions and 128 deletions

View File

@ -1,9 +1,10 @@
import { render, waitFor, screen, fireEvent } from '@testing-library/react';
import { render, waitFor, screen, fireEvent, waitForElementToBeRemoved, within, Matcher } from '@testing-library/react';
import { merge, uniqueId } from 'lodash';
import React from 'react';
import { DeepPartial } from 'react-hook-form';
import { Provider } from 'react-redux';
import { Observable } from 'rxjs';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { DataSourcePluginMeta } from '@grafana/data';
@ -127,19 +128,93 @@ const renderWithContext = async (
} as unknown as BackendSrv;
const grafanaContext = getGrafanaContextMock({ backend });
setDataSourceSrv(new MockDataSourceSrv(datasources));
const dsServer = new MockDataSourceSrv(datasources);
dsServer.get = (name: string) => {
const dsApi = new MockDataSourceApi(name);
dsApi.components = {
QueryEditor: () => <>{name} query editor</>,
};
return Promise.resolve(dsApi);
};
render(
setDataSourceSrv(dsServer);
const renderResult = render(
<Provider store={configureStore({})}>
<GrafanaContext.Provider value={grafanaContext}>
<CorrelationsPage />
</GrafanaContext.Provider>
</Provider>
</Provider>,
{
queries: {
/**
* Gets all the rows in the table having the given text in the given column
*/
queryRowsByCellValue: (
container: HTMLElement,
columnName: Matcher,
textValue: Matcher
): HTMLTableRowElement[] => {
const table = within(container).getByRole('table');
const headers = within(table).getAllByRole('columnheader');
const headerIndex = headers.findIndex((h) => {
return within(h).queryByText(columnName);
});
// the first rowgroup is the header
const tableBody = within(table).getAllByRole('rowgroup')[1];
return within(tableBody)
.getAllByRole<HTMLTableRowElement>('row')
.filter((row) => {
const rowCells = within(row).getAllByRole('cell');
const cell = rowCells[headerIndex];
return within(cell).queryByText(textValue);
});
},
/**
* Gets all the cells in the table for the given column name
*/
queryCellsByColumnName: (container: HTMLElement, columnName: Matcher) => {
const table = within(container).getByRole('table');
const headers = within(table).getAllByRole('columnheader');
const headerIndex = headers.findIndex((h) => {
return within(h).queryByText(columnName);
});
const tbody = table.querySelector('tbody');
if (!tbody) {
return [];
}
return within(tbody)
.getAllByRole('row')
.map((r) => {
const cells = within(r).getAllByRole<HTMLTableCellElement>('cell');
return cells[headerIndex];
});
},
/**
* Gets the table header cell matching the given name
*/
getHeaderByName: (container: HTMLElement, columnName: Matcher): HTMLTableCellElement => {
const table = within(container).getByRole('table');
const headers = within(table).getAllByRole<HTMLTableCellElement>('columnheader');
const header = headers.find((h) => {
return within(h).queryByText(columnName);
});
if (!header) {
throw new Error(`Could not find header with name ${columnName}`);
}
return header;
},
},
}
);
await waitFor(() => {
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
return renderResult;
};
beforeAll(() => {
@ -194,6 +269,9 @@ describe('CorrelationsPage', () => {
fireEvent.click(CTAButton);
// wait for the form to be rendered and query editor to be mounted
await waitForElementToBeRemoved(() => screen.queryByText(/loading query editor/i));
// form's submit button
expect(screen.getByRole('button', { name: /add$/i })).toBeInTheDocument();
});
@ -218,12 +296,14 @@ describe('CorrelationsPage', () => {
fireEvent.keyDown(screen.getByLabelText(/^target$/i), { keyCode: 40 });
fireEvent.click(screen.getByText('prometheus'));
fireEvent.change(screen.getByRole('textbox', { name: /target field/i }), { target: { value: 'Line' } });
await waitForElementToBeRemoved(() => screen.queryByText(/loading query editor/i));
fireEvent.click(screen.getByRole('button', { name: /add$/i }));
// Waits for the form to be removed, meaning the correlation got successfully saved
await waitFor(() => {
expect(screen.queryByRole('button', { name: /add$/i })).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(() => screen.queryByRole('button', { name: /add$/i }));
// the table showing correlations should have appeared
expect(screen.getByRole('table')).toBeInTheDocument();
@ -231,8 +311,12 @@ describe('CorrelationsPage', () => {
});
describe('With correlations', () => {
let queryRowsByCellValue: (columnName: Matcher, textValue: Matcher) => HTMLTableRowElement[];
let getHeaderByName: (columnName: Matcher) => HTMLTableCellElement;
let queryCellsByColumnName: (columnName: Matcher) => HTMLTableCellElement[];
beforeEach(async () => {
await renderWithContext(
const renderResult = await renderWithContext(
{
loki: mockDataSource(
{
@ -275,16 +359,53 @@ describe('CorrelationsPage', () => {
}
),
},
[{ sourceUID: 'loki', targetUID: 'loki', uid: '1', label: 'Some label' }]
[
{
sourceUID: 'loki',
targetUID: 'loki',
uid: '1',
label: 'Some label',
config: { field: 'line', target: {}, type: 'query' },
},
{
sourceUID: 'prometheus',
targetUID: 'loki',
uid: '2',
label: 'Prometheus to Loki',
config: { field: 'label', target: {}, type: 'query' },
},
]
);
queryRowsByCellValue = renderResult.queryRowsByCellValue;
queryCellsByColumnName = renderResult.queryCellsByColumnName;
getHeaderByName = renderResult.getHeaderByName;
});
it('shows a table with correlations', async () => {
await renderWithContext();
expect(screen.getByRole('table')).toBeInTheDocument();
});
it('correctly sorts by source', async () => {
const sourceHeader = getHeaderByName('Source');
fireEvent.click(sourceHeader);
let cells = queryCellsByColumnName('Source');
cells.forEach((cell, i, allCells) => {
const prevCell = allCells[i - 1];
if (prevCell && prevCell.textContent) {
expect(cell.textContent?.localeCompare(prevCell.textContent)).toBeGreaterThanOrEqual(0);
}
});
fireEvent.click(sourceHeader);
cells = queryCellsByColumnName('Source');
cells.forEach((cell, i, allCells) => {
const prevCell = allCells[i - 1];
if (prevCell && prevCell.textContent) {
expect(cell.textContent?.localeCompare(prevCell.textContent)).toBeLessThanOrEqual(0);
}
});
});
it('correctly adds correlations', async () => {
const addNewButton = screen.getByRole('button', { name: /add new/i });
expect(addNewButton).toBeInTheDocument();
@ -295,18 +416,20 @@ describe('CorrelationsPage', () => {
// set source datasource picker value
fireEvent.keyDown(screen.getByLabelText(/^source$/i), { keyCode: 40 });
fireEvent.click(screen.getByText('prometheus'));
fireEvent.click(within(screen.getByLabelText('Select options menu')).getByText('prometheus'));
// set target datasource picker value
fireEvent.keyDown(screen.getByLabelText(/^target$/i), { keyCode: 40 });
fireEvent.click(screen.getByText('elastic'));
fireEvent.change(screen.getByRole('textbox', { name: /target field/i }), { target: { value: 'Line' } });
await waitForElementToBeRemoved(() => screen.queryByText(/loading query editor/i));
fireEvent.click(screen.getByRole('button', { name: /add$/i }));
// the form should get removed after successful submissions
await waitFor(() => {
expect(screen.queryByRole('button', { name: /add$/i })).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(() => screen.queryByRole('button', { name: /add$/i }));
});
it('correctly closes the form when clicking on the close icon', async () => {
@ -316,35 +439,37 @@ describe('CorrelationsPage', () => {
fireEvent.click(screen.getByRole('button', { name: /close$/i }));
await waitFor(() => {
expect(screen.queryByRole('button', { name: /add$/i })).not.toBeInTheDocument();
});
expect(screen.queryByRole('button', { name: /add$/i })).not.toBeInTheDocument();
});
it('correctly deletes correlations', async () => {
// A row with the correlation should exist
expect(screen.getByRole('cell', { name: /some label/i })).toBeInTheDocument();
const deleteButton = screen.getByRole('button', { name: /delete correlation/i });
const tableRows = queryRowsByCellValue('Source', 'loki');
const deleteButton = within(tableRows[0]).getByRole('button', { name: /delete correlation/i });
expect(deleteButton).toBeInTheDocument();
fireEvent.click(deleteButton);
const confirmButton = screen.getByRole('button', { name: /delete$/i });
const confirmButton = within(tableRows[0]).getByRole('button', { name: /delete$/i });
expect(confirmButton).toBeInTheDocument();
fireEvent.click(confirmButton);
await waitFor(() => {
expect(screen.queryByRole('cell', { name: /some label/i })).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(() => screen.queryByRole('cell', { name: /some label$/i }));
});
it('correctly edits correlations', async () => {
const rowExpanderButton = screen.getByRole('button', { name: /toggle row expanded/i });
const tableRows = queryRowsByCellValue('Source', 'loki');
const rowExpanderButton = within(tableRows[0]).getByRole('button', { name: /toggle row expanded/i });
fireEvent.click(rowExpanderButton);
await waitForElementToBeRemoved(() => screen.queryByText(/loading query editor/i));
fireEvent.change(screen.getByRole('textbox', { name: /label/i }), { target: { value: 'edited label' } });
fireEvent.change(screen.getByRole('textbox', { name: /description/i }), {
target: { value: 'edited description' },
@ -361,7 +486,15 @@ describe('CorrelationsPage', () => {
});
describe('Read only correlations', () => {
const correlations = [{ sourceUID: 'loki', targetUID: 'loki', uid: '1', label: 'Some label' }];
const correlations: Correlation[] = [
{
sourceUID: 'loki',
targetUID: 'loki',
uid: '1',
label: 'Some label',
config: { field: 'line', target: {}, type: 'query' },
},
];
beforeEach(async () => {
await renderWithContext(
@ -393,6 +526,9 @@ describe('CorrelationsPage', () => {
fireEvent.click(rowExpanderButton);
// wait for the form to be rendered and query editor to be mounted
await waitForElementToBeRemoved(() => screen.queryByText(/loading query editor/i));
// form elements should be readonly
const labelInput = screen.getByRole('textbox', { name: /label/i });
expect(labelInput).toBeInTheDocument();

View File

@ -148,7 +148,7 @@ export default function CorrelationsPage() {
<Table
renderExpandedRow={({ target, source, ...correlation }) => (
<EditCorrelationForm
defaultValues={{ sourceUID: source.uid, ...correlation }}
correlation={{ ...correlation, sourceUID: source.uid, targetUID: target.uid }}
onUpdated={fetchCorrelations}
readOnly={isSourceReadOnly({ source }) || !canWriteCorrelations}
/>

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import React, { useEffect } from 'react';
import { Controller } from 'react-hook-form';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
@ -12,7 +12,6 @@ import { useCorrelations } from '../useCorrelations';
import { CorrelationDetailsFormPart } from './CorrelationDetailsFormPart';
import { FormDTO } from './types';
import { useCorrelationForm } from './useCorrelationForm';
const getStyles = (theme: GrafanaTheme2) => ({
panelContainer: css`
@ -57,62 +56,75 @@ export const AddCorrelationForm = ({ onClose, onCreated }: Props) => {
}
}, [error, loading, value, onCreated]);
const { control, handleSubmit, register, errors } = useCorrelationForm<FormDTO>({ onSubmit: execute });
const methods = useForm<FormDTO>({ defaultValues: { config: { type: 'query', target: {} } } });
return (
<PanelContainer className={styles.panelContainer}>
<CloseButton onClick={onClose} />
<form onSubmit={handleSubmit}>
<div className={styles.horizontalGroup}>
<Controller
control={control}
name="sourceUID"
rules={{
required: { value: true, message: 'This field is required.' },
validate: {
writable: (uid: string) =>
!getDatasourceSrv().getInstanceSettings(uid)?.readOnly || "Source can't be a read-only data source.",
},
}}
render={({ field: { onChange, value } }) => (
<Field label="Source" htmlFor="source" invalid={!!errors.sourceUID} error={errors.sourceUID?.message}>
<DataSourcePicker
onChange={withDsUID(onChange)}
noDefault
current={value}
inputId="source"
width={32}
/>
</Field>
)}
/>
<div className={styles.linksToContainer}>Links to</div>
<Controller
control={control}
name="targetUID"
rules={{ required: { value: true, message: 'This field is required.' } }}
render={({ field: { onChange, value } }) => (
<Field label="Target" htmlFor="target" invalid={!!errors.targetUID} error={errors.targetUID?.message}>
<DataSourcePicker
onChange={withDsUID(onChange)}
noDefault
current={value}
inputId="target"
width={32}
/>
</Field>
)}
/>
</div>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(execute)}>
<div className={styles.horizontalGroup}>
<Controller
control={methods.control}
name="sourceUID"
rules={{
required: { value: true, message: 'This field is required.' },
validate: {
writable: (uid: string) =>
!getDatasourceSrv().getInstanceSettings(uid)?.readOnly ||
"Source can't be a read-only data source.",
},
}}
render={({ field: { onChange, value } }) => (
<Field
label="Source"
htmlFor="source"
invalid={!!methods.formState.errors.sourceUID}
error={methods.formState.errors.sourceUID?.message}
>
<DataSourcePicker
onChange={withDsUID(onChange)}
noDefault
current={value}
inputId="source"
width={32}
/>
</Field>
)}
/>
<div className={styles.linksToContainer}>Links to</div>
<Controller
control={methods.control}
name="targetUID"
rules={{ required: { value: true, message: 'This field is required.' } }}
render={({ field: { onChange, value } }) => (
<Field
label="Target"
htmlFor="target"
invalid={!!methods.formState.errors.targetUID}
error={methods.formState.errors.targetUID?.message}
>
<DataSourcePicker
onChange={withDsUID(onChange)}
noDefault
current={value}
inputId="target"
width={32}
/>
</Field>
)}
/>
</div>
<CorrelationDetailsFormPart register={register} />
<CorrelationDetailsFormPart />
<HorizontalGroup justify="flex-end">
<Button variant="primary" icon={loading ? 'fa fa-spinner' : 'plus'} type="submit" disabled={loading}>
Add
</Button>
</HorizontalGroup>
</form>
<HorizontalGroup justify="flex-end">
<Button variant="primary" icon={loading ? 'fa fa-spinner' : 'plus'} type="submit" disabled={loading}>
Add
</Button>
</HorizontalGroup>
</form>
</FormProvider>
</PanelContainer>
);
};

View File

@ -1,13 +1,16 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { RegisterOptions, UseFormRegisterReturn } from 'react-hook-form';
import { useFormContext, useWatch } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { Field, Input, TextArea, useStyles2 } from '@grafana/ui';
import { EditFormDTO } from './types';
import { Correlation } from '../types';
const getInputId = (inputName: string, correlation?: EditFormDTO) => {
import { QueryEditorField } from './QueryEditorField';
import { FormDTO } from './types';
const getInputId = (inputName: string, correlation?: CorrelationBaseData) => {
if (!correlation) {
return inputName;
}
@ -16,9 +19,6 @@ const getInputId = (inputName: string, correlation?: EditFormDTO) => {
};
const getStyles = (theme: GrafanaTheme2) => ({
marginless: css`
margin: 0;
`,
label: css`
max-width: ${theme.spacing(32)};
`,
@ -27,17 +27,24 @@ const getStyles = (theme: GrafanaTheme2) => ({
`,
});
type CorrelationBaseData = Pick<Correlation, 'uid' | 'sourceUID' | 'targetUID'>;
interface Props {
register: (path: 'label' | 'description', options?: RegisterOptions) => UseFormRegisterReturn;
readOnly?: boolean;
correlation?: EditFormDTO;
correlation?: CorrelationBaseData;
}
export function CorrelationDetailsFormPart({ register, readOnly = false, correlation }: Props) {
export function CorrelationDetailsFormPart({ readOnly = false, correlation }: Props) {
const styles = useStyles2(getStyles);
const {
register,
formState: { errors },
} = useFormContext<FormDTO>();
const targetUID: string | undefined = useWatch({ name: 'targetUID' }) || correlation?.targetUID;
return (
<>
<input type="hidden" {...register('config.type')} />
<Field label="Label" className={styles.label}>
<Input
id={getInputId('label', correlation)}
@ -50,10 +57,31 @@ export function CorrelationDetailsFormPart({ register, readOnly = false, correla
<Field
label="Description"
// the Field component automatically adds margin to itself, so we are forced to workaround it by overriding its styles
className={cx(readOnly && styles.marginless, styles.description)}
className={cx(styles.description)}
>
<TextArea id={getInputId('description', correlation)} {...register('description')} readOnly={readOnly} />
</Field>
<Field
label="Target field"
className={styles.label}
invalid={!!errors?.config?.field}
error={errors?.config?.field?.message}
>
<Input
id={getInputId('field', correlation)}
{...register('config.field', { required: 'This field is required.' })}
readOnly={readOnly}
/>
</Field>
<QueryEditorField
name="config.target"
dsUid={targetUID}
invalid={!!errors?.config?.target}
// @ts-expect-error react-hook-form's errors do not work well with object types
error={errors?.config?.target?.message}
/>
</>
);
}

View File

@ -1,45 +1,52 @@
import React, { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Button, HorizontalGroup } from '@grafana/ui';
import { Correlation } from '../types';
import { useCorrelations } from '../useCorrelations';
import { CorrelationDetailsFormPart } from './CorrelationDetailsFormPart';
import { EditFormDTO } from './types';
import { useCorrelationForm } from './useCorrelationForm';
interface Props {
onUpdated: () => void;
defaultValues: EditFormDTO;
correlation: Correlation;
readOnly?: boolean;
}
export const EditCorrelationForm = ({ onUpdated, defaultValues, readOnly = false }: Props) => {
export const EditCorrelationForm = ({ onUpdated, correlation, readOnly = false }: Props) => {
const {
update: { execute, loading, error, value },
} = useCorrelations();
const onSubmit = (data: EditFormDTO) => {
return execute({ ...data, sourceUID: correlation.sourceUID, uid: correlation.uid });
};
useEffect(() => {
if (!error && !loading && value) {
onUpdated();
}
}, [error, loading, value, onUpdated]);
const { handleSubmit, register } = useCorrelationForm<EditFormDTO>({ onSubmit: execute, defaultValues });
const { uid, sourceUID, targetUID, ...otherCorrelation } = correlation;
const methods = useForm<EditFormDTO>({ defaultValues: otherCorrelation });
return (
<form onSubmit={readOnly ? (e) => e.preventDefault() : handleSubmit}>
<input type="hidden" {...register('uid')} />
<input type="hidden" {...register('sourceUID')} />
<CorrelationDetailsFormPart register={register} readOnly={readOnly} correlation={defaultValues} />
<FormProvider {...methods}>
<form onSubmit={readOnly ? (e) => e.preventDefault() : methods.handleSubmit(onSubmit)}>
<CorrelationDetailsFormPart readOnly={readOnly} correlation={correlation} />
{!readOnly && (
<HorizontalGroup justify="flex-end">
<Button variant="primary" icon={loading ? 'fa fa-spinner' : 'save'} type="submit" disabled={loading}>
Save
</Button>
</HorizontalGroup>
)}
</form>
{!readOnly && (
<HorizontalGroup justify="flex-end">
<Button variant="primary" icon={loading ? 'fa fa-spinner' : 'save'} type="submit" disabled={loading}>
Save
</Button>
</HorizontalGroup>
)}
</form>
</FormProvider>
);
};

View File

@ -0,0 +1,66 @@
import { render, screen } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { setDataSourceSrv } from '@grafana/runtime';
import { MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
import { QueryEditorField } from './QueryEditorField';
const Wrapper = ({ children }: { children: ReactNode }) => {
const methods = useForm();
return <FormProvider {...methods}>{children}</FormProvider>;
};
const defaultGetHandler = async (name: string) => {
const dsApi = new MockDataSourceApi(name);
dsApi.components = {
QueryEditor: () => <>{name} query editor</>,
};
return dsApi;
};
const renderWithContext = async (
children: ReactNode,
getHandler: (name: string) => Promise<MockDataSourceApi> = defaultGetHandler
) => {
const dsServer = new MockDataSourceSrv({});
dsServer.get = getHandler;
setDataSourceSrv(dsServer);
render(<Wrapper>{children}</Wrapper>);
};
describe('QueryEditorField', () => {
it('should render the query editor', async () => {
renderWithContext(<QueryEditorField name="query" dsUid="test" />);
expect(await screen.findByText('test query editor')).toBeInTheDocument();
});
it("shows an error alert when datasource can't be loaded", async () => {
renderWithContext(<QueryEditorField name="query" dsUid="something" />, () => {
throw new Error('Unable to load datasource');
});
expect(await screen.findByRole('alert', { name: 'Error loading data source' })).toBeInTheDocument();
});
it('shows an info alert when no datasource is selected', async () => {
renderWithContext(<QueryEditorField name="query" />);
expect(await screen.findByRole('alert', { name: 'No data source selected' })).toBeInTheDocument();
});
it('shows an info alert when datasaource does not export a query editor', async () => {
renderWithContext(<QueryEditorField name="query" dsUid="something" />, async (name) => {
return new MockDataSourceApi(name);
});
expect(
await screen.findByRole('alert', { name: 'Data source does not export a query editor.' })
).toBeInTheDocument();
});
});

View File

@ -0,0 +1,61 @@
import React from 'react';
import { Controller } from 'react-hook-form';
import { useAsync } from 'react-use';
import { getDataSourceSrv } from '@grafana/runtime';
import { Field, LoadingPlaceholder, Alert } from '@grafana/ui';
interface Props {
dsUid?: string;
name: string;
invalid?: boolean;
error?: string;
}
export const QueryEditorField = ({ dsUid, invalid, error, name }: Props) => {
const {
value: datasource,
loading: dsLoading,
error: dsError,
} = useAsync(async () => {
if (!dsUid) {
return;
}
return getDataSourceSrv().get(dsUid);
}, [dsUid]);
const QueryEditor = datasource?.components?.QueryEditor;
return (
<Field label="Query" invalid={invalid} error={error}>
<Controller
name={name}
rules={{
validate: {
hasQueryEditor: () =>
QueryEditor !== undefined || 'The selected target data source must export a query editor.',
},
}}
render={({ field: { value, onChange } }) => {
if (dsLoading) {
return <LoadingPlaceholder text="Loading query editor..." />;
}
if (dsError) {
return <Alert title="Error loading data source">The selected data source could not be loaded.</Alert>;
}
if (!datasource) {
return (
<Alert title="No data source selected" severity="info">
Please select a target data source first.
</Alert>
);
}
if (!QueryEditor) {
return <Alert title="Data source does not export a query editor."></Alert>;
}
return <QueryEditor onRunQuery={() => {}} onChange={onChange} datasource={datasource} query={value} />;
}}
/>
</Field>
);
};

View File

@ -1,11 +1,11 @@
import { Correlation } from '../types';
import { CorrelationConfig } from '../types';
export interface FormDTO {
sourceUID: string;
targetUID: string;
label: string;
description: string;
config: CorrelationConfig;
}
type FormDTOWithoutTarget = Omit<FormDTO, 'targetUID'>;
export type EditFormDTO = Partial<FormDTOWithoutTarget> & Pick<FormDTO, 'sourceUID'> & { uid: Correlation['uid'] };
export type EditFormDTO = Omit<FormDTO, 'targetUID' | 'sourceUID'>;

View File

@ -1,21 +0,0 @@
import { DeepPartial, FieldValues, SubmitHandler, UnpackNestedValue, useForm } from 'react-hook-form';
interface UseCorrelationFormOptions<T extends FieldValues> {
onSubmit: SubmitHandler<T>;
defaultValues?: UnpackNestedValue<DeepPartial<T>>;
}
export const useCorrelationForm = <T extends FieldValues>({
onSubmit,
defaultValues,
}: UseCorrelationFormOptions<T>) => {
const {
handleSubmit: submit,
control,
register,
formState: { errors },
} = useForm<T>({ defaultValues });
const handleSubmit = submit(onSubmit);
return { control, handleSubmit, register, errors };
};

View File

@ -4,12 +4,20 @@ export interface AddCorrelationResponse {
export type GetCorrelationsResponse = Correlation[];
type CorrelationConfigType = 'query';
export interface CorrelationConfig {
field: string;
target: object;
type: CorrelationConfigType;
}
export interface Correlation {
uid: string;
sourceUID: string;
targetUID: string;
label?: string;
description?: string;
config: CorrelationConfig;
}
export type RemoveCorrelationParams = Pick<Correlation, 'sourceUID' | 'uid'>;