diff --git a/public/app/features/correlations/CorrelationsPage.test.tsx b/public/app/features/correlations/CorrelationsPage.test.tsx
index 4d8f484441d..f143a2bed43 100644
--- a/public/app/features/correlations/CorrelationsPage.test.tsx
+++ b/public/app/features/correlations/CorrelationsPage.test.tsx
@@ -271,12 +271,12 @@ describe('CorrelationsPage', () => {
mocks.reportInteraction.mockClear();
});
- it('shows CTA', async () => {
+ it('shows the first page of the wizard', async () => {
const CTAButton = await screen.findByRole('button', { name: /add correlation/i });
expect(CTAButton).toBeInTheDocument();
// insert form should not be present
- expect(screen.queryByRole('button', { name: /add$/i })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /next$/i })).not.toBeInTheDocument();
// "add new" button is the button on the top of the page, not visible when the CTA is rendered
expect(screen.queryByRole('button', { name: /add new$/i })).not.toBeInTheDocument();
@@ -286,8 +286,8 @@ describe('CorrelationsPage', () => {
await userEvent.click(CTAButton);
- // form's submit button
- expect(await screen.findByRole('button', { name: /add$/i })).toBeInTheDocument();
+ // form's next button
+ expect(await screen.findByRole('button', { name: /next$/i })).toBeInTheDocument();
});
it('correctly adds first correlation', async () => {
@@ -299,22 +299,27 @@ describe('CorrelationsPage', () => {
await userEvent.click(CTAButton);
+ // step 1: label and description
await userEvent.clear(screen.getByRole('textbox', { name: /label/i }));
await userEvent.type(screen.getByRole('textbox', { name: /label/i }), 'A Label');
await userEvent.clear(screen.getByRole('textbox', { name: /description/i }));
await userEvent.type(screen.getByRole('textbox', { name: /description/i }), 'A Description');
+ await userEvent.click(await screen.findByRole('button', { name: /next$/i }));
- // set source datasource picker value
- fireEvent.keyDown(screen.getByLabelText(/^source$/i), { keyCode: 40 });
- await userEvent.click(screen.getByText('loki'));
-
+ // step 2:
// set target datasource picker value
- fireEvent.keyDown(screen.getByLabelText(/^target$/i), { keyCode: 40 });
+ fireEvent.keyDown(screen.getByLabelText(/^target/i), { keyCode: 40 });
await userEvent.click(screen.getByText('prometheus'));
+ await userEvent.click(await screen.findByRole('button', { name: /next$/i }));
- await userEvent.clear(screen.getByRole('textbox', { name: /target field/i }));
- await userEvent.type(screen.getByRole('textbox', { name: /target field/i }), 'Line');
+ // step 3:
+ // set source datasource picker value
+ fireEvent.keyDown(screen.getByLabelText(/^source/i), { keyCode: 40 });
+ await userEvent.click(screen.getByText('loki'));
+ await userEvent.click(await screen.findByRole('button', { name: /add$/i }));
+ await userEvent.clear(screen.getByRole('textbox', { name: /results field/i }));
+ await userEvent.type(screen.getByRole('textbox', { name: /results field/i }), 'Line');
await userEvent.click(await screen.findByRole('button', { name: /add$/i }));
expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_added');
@@ -432,21 +437,26 @@ describe('CorrelationsPage', () => {
expect(addNewButton).toBeInTheDocument();
await userEvent.click(addNewButton);
+ // step 1:
await userEvent.clear(screen.getByRole('textbox', { name: /label/i }));
await userEvent.type(screen.getByRole('textbox', { name: /label/i }), 'A Label');
await userEvent.clear(screen.getByRole('textbox', { name: /description/i }));
await userEvent.type(screen.getByRole('textbox', { name: /description/i }), 'A Description');
+ await userEvent.click(await screen.findByRole('button', { name: /next$/i }));
+ // step 2:
+ // set target datasource picker value
+ fireEvent.keyDown(screen.getByLabelText(/^target/i), { keyCode: 40 });
+ await userEvent.click(screen.getByText('elastic'));
+ await userEvent.click(await screen.findByRole('button', { name: /next$/i }));
+
+ // step 3:
// set source datasource picker value
- fireEvent.keyDown(screen.getByLabelText(/^source$/i), { keyCode: 40 });
+ fireEvent.keyDown(screen.getByLabelText(/^source/i), { keyCode: 40 });
await userEvent.click(within(screen.getByLabelText('Select options menu')).getByText('prometheus'));
- // set target datasource picker value
- fireEvent.keyDown(screen.getByLabelText(/^target$/i), { keyCode: 40 });
- await userEvent.click(screen.getByText('elastic'));
-
- await userEvent.clear(screen.getByRole('textbox', { name: /target field/i }));
- await userEvent.type(screen.getByRole('textbox', { name: /target field/i }), 'Line');
+ await userEvent.clear(screen.getByRole('textbox', { name: /results field/i }));
+ await userEvent.type(screen.getByRole('textbox', { name: /results field/i }), 'Line');
await userEvent.click(screen.getByRole('button', { name: /add$/i }));
@@ -506,6 +516,8 @@ describe('CorrelationsPage', () => {
expect(screen.queryByRole('cell', { name: /edited label$/i })).not.toBeInTheDocument();
+ await userEvent.click(screen.getByRole('button', { name: /next$/i }));
+ await userEvent.click(screen.getByRole('button', { name: /next$/i }));
await userEvent.click(screen.getByRole('button', { name: /save$/i }));
expect(await screen.findByRole('cell', { name: /edited label$/i })).toBeInTheDocument();
diff --git a/public/app/features/correlations/CorrelationsPage.tsx b/public/app/features/correlations/CorrelationsPage.tsx
index 303d0827862..9f1904f07ae 100644
--- a/public/app/features/correlations/CorrelationsPage.tsx
+++ b/public/app/features/correlations/CorrelationsPage.tsx
@@ -145,7 +145,6 @@ export default function CorrelationsPage() {
-
Correlations
Define how data living in different data sources relates to each other.
{canWriteCorrelations && data?.length !== 0 && data !== undefined && !isAdding && (
diff --git a/public/app/features/correlations/Forms/AddCorrelationForm.tsx b/public/app/features/correlations/Forms/AddCorrelationForm.tsx
index 3be1bae60ed..21664725530 100644
--- a/public/app/features/correlations/Forms/AddCorrelationForm.tsx
+++ b/public/app/features/correlations/Forms/AddCorrelationForm.tsx
@@ -1,16 +1,18 @@
import { css } from '@emotion/css';
import React, { useEffect } from 'react';
-import { Controller, FormProvider, useForm } from 'react-hook-form';
-import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
-import { DataSourcePicker } from '@grafana/runtime';
-import { Button, Field, HorizontalGroup, PanelContainer, useStyles2 } from '@grafana/ui';
+import { GrafanaTheme2 } from '@grafana/data';
+import { PanelContainer, useStyles2 } from '@grafana/ui';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
-import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+import { Wizard } from '../components/Wizard';
import { useCorrelations } from '../useCorrelations';
-import { CorrelationDetailsFormPart } from './CorrelationDetailsFormPart';
+import { ConfigureCorrelationBasicInfoForm } from './ConfigureCorrelationBasicInfoForm';
+import { ConfigureCorrelationSourceForm } from './ConfigureCorrelationSourceForm';
+import { ConfigureCorrelationTargetForm } from './ConfigureCorrelationTargetForm';
+import { CorrelationFormNavigation } from './CorrelationFormNavigation';
+import { CorrelationsFormContextProvider } from './correlationsFormContext';
import { FormDTO } from './types';
const getStyles = (theme: GrafanaTheme2) => ({
@@ -19,20 +21,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
padding: ${theme.spacing(1)};
margin-bottom: ${theme.spacing(2)};
`,
- linksToContainer: css`
- flex-grow: 1;
- /* This is the width of the textarea minus the sum of the label&description fields,
- * so that this element takes exactly the remaining space and the inputs will be
- * nicely aligned with the textarea
- **/
- max-width: ${theme.spacing(80 - 64)};
- margin-top: ${theme.spacing(3)};
- text-align: right;
- padding-right: ${theme.spacing(1)};
- `,
- // we can't use HorizontalGroup because it wraps elements in divs and sets margins on them
- horizontalGroup: css`
- display: flex;
+ infoBox: css`
+ margin-top: 20px; // give space for close button
`,
});
@@ -41,8 +31,6 @@ interface Props {
onCreated: () => void;
}
-const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
-
export const AddCorrelationForm = ({ onClose, onCreated }: Props) => {
const styles = useStyles2(getStyles);
@@ -56,75 +44,19 @@ export const AddCorrelationForm = ({ onClose, onCreated }: Props) => {
}
}, [error, loading, value, onCreated]);
- const methods = useForm({ defaultValues: { config: { type: 'query', target: {} } } });
+ const defaultValues: Partial = { config: { type: 'query', target: {}, field: '' } };
return (
-
-
-
+
+
+ defaultValues={defaultValues}
+ pages={[ConfigureCorrelationBasicInfoForm, ConfigureCorrelationTargetForm, ConfigureCorrelationSourceForm]}
+ navigation={CorrelationFormNavigation}
+ onSubmit={execute}
+ />
+
);
};
diff --git a/public/app/features/correlations/Forms/ConfigureCorrelationBasicInfoForm.tsx b/public/app/features/correlations/Forms/ConfigureCorrelationBasicInfoForm.tsx
new file mode 100644
index 00000000000..00a120c5817
--- /dev/null
+++ b/public/app/features/correlations/Forms/ConfigureCorrelationBasicInfoForm.tsx
@@ -0,0 +1,57 @@
+import { css, cx } from '@emotion/css';
+import React from 'react';
+import { useFormContext } from 'react-hook-form';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { Field, FieldSet, Input, TextArea, useStyles2 } from '@grafana/ui';
+
+import { useCorrelationsFormContext } from './correlationsFormContext';
+import { FormDTO } from './types';
+import { getInputId } from './utils';
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ label: css`
+ max-width: ${theme.spacing(80)};
+ `,
+ description: css`
+ max-width: ${theme.spacing(80)};
+ `,
+});
+
+export const ConfigureCorrelationBasicInfoForm = () => {
+ const { register, formState } = useFormContext();
+ const styles = useStyles2(getStyles);
+ const { correlation, readOnly } = useCorrelationsFormContext();
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx b/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx
new file mode 100644
index 00000000000..e6a504d0368
--- /dev/null
+++ b/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx
@@ -0,0 +1,79 @@
+import { css } from '@emotion/css';
+import React from 'react';
+import { Controller, useFormContext } from 'react-hook-form';
+
+import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
+import { DataSourcePicker } from '@grafana/runtime';
+import { Field, FieldSet, Input, useStyles2 } from '@grafana/ui';
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+
+import { useCorrelationsFormContext } from './correlationsFormContext';
+import { getInputId } from './utils';
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ label: css`
+ max-width: ${theme.spacing(80)};
+ `,
+});
+
+export const ConfigureCorrelationSourceForm = () => {
+ const { control, formState, register } = useFormContext();
+ const styles = useStyles2(getStyles);
+ const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
+
+ const { correlation, readOnly } = useCorrelationsFormContext();
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx b/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx
new file mode 100644
index 00000000000..58687a4b8a7
--- /dev/null
+++ b/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { Controller, useFormContext, useWatch } from 'react-hook-form';
+
+import { DataSourceInstanceSettings } from '@grafana/data';
+import { DataSourcePicker } from '@grafana/runtime';
+import { Field, FieldSet } from '@grafana/ui';
+
+import { QueryEditorField } from './QueryEditorField';
+import { useCorrelationsFormContext } from './correlationsFormContext';
+
+export const ConfigureCorrelationTargetForm = () => {
+ const { control, formState } = useFormContext();
+ const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
+ const { correlation } = useCorrelationsFormContext();
+ const targetUID: string | undefined = useWatch({ name: 'targetUID' }) || correlation?.targetUID;
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/public/app/features/correlations/Forms/CorrelationDetailsFormPart.tsx b/public/app/features/correlations/Forms/CorrelationDetailsFormPart.tsx
deleted file mode 100644
index 58de7a44c13..00000000000
--- a/public/app/features/correlations/Forms/CorrelationDetailsFormPart.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import { css, cx } from '@emotion/css';
-import React from 'react';
-import { useFormContext, useWatch } from 'react-hook-form';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { Field, Input, TextArea, useStyles2 } from '@grafana/ui';
-
-import { Correlation } from '../types';
-
-import { QueryEditorField } from './QueryEditorField';
-import { FormDTO } from './types';
-
-const getInputId = (inputName: string, correlation?: CorrelationBaseData) => {
- if (!correlation) {
- return inputName;
- }
-
- return `${inputName}_${correlation.sourceUID}-${correlation.uid}`;
-};
-
-const getStyles = (theme: GrafanaTheme2) => ({
- label: css`
- max-width: ${theme.spacing(32)};
- `,
- description: css`
- max-width: ${theme.spacing(80)};
- `,
-});
-
-type CorrelationBaseData = Pick;
-interface Props {
- readOnly?: boolean;
- correlation?: CorrelationBaseData;
-}
-
-export function CorrelationDetailsFormPart({ readOnly = false, correlation }: Props) {
- const styles = useStyles2(getStyles);
- const {
- register,
- formState: { errors },
- } = useFormContext();
- const targetUID: string | undefined = useWatch({ name: 'targetUID' }) || correlation?.targetUID;
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx b/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx
new file mode 100644
index 00000000000..d3ce7888e61
--- /dev/null
+++ b/public/app/features/correlations/Forms/CorrelationFormNavigation.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+
+import { Button, HorizontalGroup } from '@grafana/ui';
+
+import { useWizardContext } from '../components/Wizard/wizardContext';
+
+import { useCorrelationsFormContext } from './correlationsFormContext';
+
+export const CorrelationFormNavigation = () => {
+ const { currentPage, prevPage, isLastPage } = useWizardContext();
+ const { readOnly, loading, correlation } = useCorrelationsFormContext();
+
+ const LastPageNext = !readOnly && (
+
+ );
+
+ const NextPage = (
+
+ );
+
+ return (
+
+ {currentPage > 0 ? (
+
+ ) : undefined}
+
+ {isLastPage ? LastPageNext : NextPage}
+
+ );
+};
diff --git a/public/app/features/correlations/Forms/EditCorrelationForm.tsx b/public/app/features/correlations/Forms/EditCorrelationForm.tsx
index 3e7014841ea..b24885aeec2 100644
--- a/public/app/features/correlations/Forms/EditCorrelationForm.tsx
+++ b/public/app/features/correlations/Forms/EditCorrelationForm.tsx
@@ -1,12 +1,14 @@
import React, { useEffect } from 'react';
-import { FormProvider, useForm } from 'react-hook-form';
-
-import { Button, HorizontalGroup } from '@grafana/ui';
+import { Wizard } from '../components/Wizard';
import { Correlation } from '../types';
import { useCorrelations } from '../useCorrelations';
-import { CorrelationDetailsFormPart } from './CorrelationDetailsFormPart';
+import { ConfigureCorrelationBasicInfoForm } from './ConfigureCorrelationBasicInfoForm';
+import { ConfigureCorrelationSourceForm } from './ConfigureCorrelationSourceForm';
+import { ConfigureCorrelationTargetForm } from './ConfigureCorrelationTargetForm';
+import { CorrelationFormNavigation } from './CorrelationFormNavigation';
+import { CorrelationsFormContextProvider } from './correlationsFormContext';
import { EditFormDTO } from './types';
interface Props {
@@ -30,23 +32,14 @@ export const EditCorrelationForm = ({ onUpdated, correlation, readOnly = false }
}
}, [error, loading, value, onUpdated]);
- const { uid, sourceUID, targetUID, ...otherCorrelation } = correlation;
-
- const methods = useForm({ defaultValues: otherCorrelation });
-
return (
-
-
-
+
+
+ defaultValues={correlation}
+ pages={[ConfigureCorrelationBasicInfoForm, ConfigureCorrelationTargetForm, ConfigureCorrelationSourceForm]}
+ onSubmit={readOnly ? (e) => () => {} : onSubmit}
+ navigation={CorrelationFormNavigation}
+ />
+
);
};
diff --git a/public/app/features/correlations/Forms/QueryEditorField.tsx b/public/app/features/correlations/Forms/QueryEditorField.tsx
index 53f425a08ff..938749a7c22 100644
--- a/public/app/features/correlations/Forms/QueryEditorField.tsx
+++ b/public/app/features/correlations/Forms/QueryEditorField.tsx
@@ -101,7 +101,24 @@ export const QueryEditorField = ({ dsUid, invalid, error, name }: Props) => {
};
return (
-
+
+ Define the query that is run when the link is clicked. You can use{' '}
+
+ variables
+ {' '}
+ to access specific field values.
+
+ }
+ invalid={invalid}
+ error={error}
+ >
({
+ loading: false,
+ correlation: undefined,
+ readOnly: false,
+});
+
+type Props = {
+ data: CorrelationsFormContextData;
+};
+
+export const CorrelationsFormContextProvider = (props: PropsWithChildren) => {
+ const { data, children } = props;
+ return {children};
+};
+
+export const useCorrelationsFormContext = () => {
+ return useContext(CorrelationsFormContext);
+};
diff --git a/public/app/features/correlations/Forms/utils.ts b/public/app/features/correlations/Forms/utils.ts
new file mode 100644
index 00000000000..be77c4f6b8b
--- /dev/null
+++ b/public/app/features/correlations/Forms/utils.ts
@@ -0,0 +1,11 @@
+import { Correlation } from '../types';
+
+type CorrelationBaseData = Pick;
+
+export const getInputId = (inputName: string, correlation?: CorrelationBaseData) => {
+ if (!correlation) {
+ return inputName;
+ }
+
+ return `${inputName}_${correlation.sourceUID}-${correlation.uid}`;
+};
diff --git a/public/app/features/correlations/components/Wizard/Wizard.test.tsx b/public/app/features/correlations/components/Wizard/Wizard.test.tsx
new file mode 100644
index 00000000000..22760c45c04
--- /dev/null
+++ b/public/app/features/correlations/components/Wizard/Wizard.test.tsx
@@ -0,0 +1,37 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+
+import { Wizard } from './Wizard';
+
+const MockPage1 = () => Page 1;
+const MockPage2 = () => Page 2;
+const MockNavigation = () => (
+
+
+
+);
+const onSubmitMock = jest.fn();
+
+describe('Wizard', () => {
+ beforeEach(() => {
+ render();
+ });
+
+ afterEach(() => {
+ onSubmitMock.mockReset();
+ });
+
+ it('Renders each page and submits at the end', async () => {
+ expect(screen.queryByText('Page 1')).toBeInTheDocument();
+ expect(screen.queryByText('Page 2')).not.toBeInTheDocument();
+ await userEvent.click(await screen.findByRole('button', { name: /next$/i }));
+ expect(onSubmitMock).not.toBeCalled();
+
+ expect(screen.queryByText('Page 1')).not.toBeInTheDocument();
+ expect(screen.queryByText('Page 2')).toBeInTheDocument();
+ await userEvent.click(await screen.findByRole('button', { name: /next$/i }));
+
+ expect(onSubmitMock).toBeCalled();
+ });
+});
diff --git a/public/app/features/correlations/components/Wizard/Wizard.tsx b/public/app/features/correlations/components/Wizard/Wizard.tsx
new file mode 100644
index 00000000000..38634a934b5
--- /dev/null
+++ b/public/app/features/correlations/components/Wizard/Wizard.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { useForm, FormProvider, FieldValues } from 'react-hook-form';
+
+import { WizardContent } from './WizardContent';
+import { WizardProps } from './types';
+import { WizardContextProvider } from './wizardContext';
+
+export function Wizard(props: WizardProps) {
+ const { defaultValues, pages, onSubmit, navigation } = props;
+ const formMethods = useForm({ defaultValues });
+ return (
+
+
+
+
+
+ );
+}
diff --git a/public/app/features/correlations/components/Wizard/WizardContent.tsx b/public/app/features/correlations/components/Wizard/WizardContent.tsx
new file mode 100644
index 00000000000..2053c40d931
--- /dev/null
+++ b/public/app/features/correlations/components/Wizard/WizardContent.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { useFormContext } from 'react-hook-form';
+
+import { useWizardContext } from './wizardContext';
+
+type Props = {
+ navigation: React.ComponentType;
+};
+
+export function WizardContent(props: Props) {
+ const { navigation } = props;
+ const { handleSubmit } = useFormContext();
+ const { CurrentPageComponent, isLastPage, nextPage, onSubmit } = useWizardContext();
+
+ const NavigationComponent = navigation;
+
+ return (
+
+ );
+}
diff --git a/public/app/features/correlations/components/Wizard/index.ts b/public/app/features/correlations/components/Wizard/index.ts
new file mode 100644
index 00000000000..7ccbf5e9774
--- /dev/null
+++ b/public/app/features/correlations/components/Wizard/index.ts
@@ -0,0 +1,2 @@
+export * from './Wizard';
+export * from './types';
diff --git a/public/app/features/correlations/components/Wizard/types.ts b/public/app/features/correlations/components/Wizard/types.ts
new file mode 100644
index 00000000000..11962faba9c
--- /dev/null
+++ b/public/app/features/correlations/components/Wizard/types.ts
@@ -0,0 +1,30 @@
+import { ComponentType } from 'react';
+import { DeepPartial, UnpackNestedValue } from 'react-hook-form';
+
+export type WizardProps = {
+ /**
+ * Initial values for the form
+ */
+ defaultValues?: UnpackNestedValue>;
+
+ /**
+ * List of steps/pages in the wizard.
+ * These are just React components. Wizard component uses react-form-hook. To access the form context
+ * inside a page component use useFormContext, e.g.
+ * const { register } = useFormContext();
+ */
+ pages: ComponentType[];
+
+ /**
+ * Navigation component to move between previous and next pages.
+ *
+ * This is a React component. To get access to navigation logic use useWizardContext, e.g.
+ * const { currentPage, prevPage, isLastPage } = useWizardContext();
+ */
+ navigation: ComponentType;
+
+ /**
+ * Final callback submitted on the last page
+ */
+ onSubmit: (data: T) => void;
+};
diff --git a/public/app/features/correlations/components/Wizard/wizardContext.tsx b/public/app/features/correlations/components/Wizard/wizardContext.tsx
new file mode 100644
index 00000000000..74b42e0e025
--- /dev/null
+++ b/public/app/features/correlations/components/Wizard/wizardContext.tsx
@@ -0,0 +1,54 @@
+import React, { createContext, PropsWithChildren, useContext, useState } from 'react';
+import { FieldValues } from 'react-hook-form';
+
+export type WizardContextProps = {
+ currentPage: number;
+ nextPage: () => void;
+ prevPage: () => void;
+ isLastPage: boolean;
+ onSubmit: (data: T) => void;
+ CurrentPageComponent: React.ComponentType;
+};
+
+export const WizardContext = createContext | undefined>(undefined);
+
+/**
+ * Dependencies provided to Wizard component required to build WizardContext
+ */
+type WizardContextProviderDeps = {
+ pages: React.ComponentType[];
+ onSubmit: (data: T) => void;
+};
+
+/**
+ * Context providing current state and logic of a Wizard. Can be used by pages and navigation components.
+ */
+export function WizardContextProvider(props: PropsWithChildren>) {
+ const [currentPage, setCurrentPage] = useState(0);
+ const { pages, onSubmit, children } = props;
+
+ return (
+ setCurrentPage(currentPage + 1),
+ prevPage: () => setCurrentPage(currentPage - 1),
+ // @ts-expect-error
+ onSubmit,
+ }}
+ >
+ {children}
+
+ );
+}
+
+export const useWizardContext = () => {
+ const ctx = useContext(WizardContext);
+
+ if (!ctx) {
+ throw new Error('useWizardContext must be used within a WizardContextProvider');
+ }
+ return ctx;
+};