Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(frontend): Adding customizable logic #3112

Merged
merged 4 commits into from
Aug 31, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 7 additions & 4 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as Sentry from '@sentry/react';
import {HistoryRouter} from 'redux-first-history/rr6';

import DashboardWrapper from 'components/DashboardWrapper/DashboardWrapper';
import CustomizationWrapper from 'components/CustomizationWrapper';
import DashboardWrapper from 'components/DashboardWrapper';
import ErrorBoundary from 'components/ErrorBoundary';
import {theme} from 'constants/Theme.constants';
import {ReduxWrapperProvider} from 'redux/ReduxWrapperProvider';
Expand All @@ -18,9 +19,11 @@ const App = () => (
<Sentry.ErrorBoundary fallback={({error}) => <ErrorBoundary error={error} />}>
<ReduxWrapperProvider>
<HistoryRouter history={history} basename={serverPathPrefix}>
<DashboardWrapper>
<BaseApp />
</DashboardWrapper>
<CustomizationWrapper>
<DashboardWrapper>
<BaseApp />
</DashboardWrapper>
</CustomizationWrapper>
</HistoryRouter>
</ReduxWrapperProvider>
</Sentry.ErrorBoundary>
Expand Down
9 changes: 2 additions & 7 deletions web/src/BaseApp.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import Router from 'components/Router';
import SettingsValuesProvider from 'providers/SettingsValues';
import {TCustomHeader} from 'components/Layout/Layout';

interface IProps {
customHeader?: TCustomHeader;
}

const BaseApp = ({customHeader}: IProps) => (
const BaseApp = () => (
<SettingsValuesProvider>
<Router customHeader={customHeader} />
<Router />
</SettingsValuesProvider>
);

Expand Down
19 changes: 19 additions & 0 deletions web/src/components/AllowButton/AllowButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Button, ButtonProps, Tooltip} from 'antd';
import {Operation, useCustomization} from 'providers/Customization/Customization.provider';

interface IProps extends ButtonProps {
operation: Operation;
}

const AllowButton = ({operation, ...props}: IProps) => {
const {getIsAllowed} = useCustomization();
const isAllowed = getIsAllowed(operation);

return (
<Tooltip title={!isAllowed ? 'You are not allowed to perform this operation' : ''}>
<Button {...props} disabled={!isAllowed} />
</Tooltip>
);
};

export default AllowButton;
3 changes: 3 additions & 0 deletions web/src/components/AllowButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {Operation} from 'providers/Customization';
// eslint-disable-next-line no-restricted-exports
export {default} from './AllowButton';
17 changes: 17 additions & 0 deletions web/src/components/CustomizationWrapper/CustomizationWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {useMemo} from 'react';
import CustomizationProvider from 'providers/Customization';

interface IProps {
children: React.ReactNode;
}

const getComponent = <T,>(id: string, fallback: React.ComponentType<T>) => fallback;
const getIsAllowed = () => true;

const CustomizationWrapper = ({children}: IProps) => {
const customizationProviderValue = useMemo(() => ({getComponent, getIsAllowed}), []);

return <CustomizationProvider value={customizationProviderValue}>{children}</CustomizationProvider>;
};

export default CustomizationWrapper;
2 changes: 2 additions & 0 deletions web/src/components/CustomizationWrapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export {default} from './CustomizationWrapper';
3 changes: 2 additions & 1 deletion web/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Logo from 'assets/Logo.svg';
import Link from 'components/Link';
import NoTracingPopover from 'components/NoTracingPopover';
import {withCustomization} from 'providers/Customization';
import * as S from './Header.styled';
import HelpMenu from './HelpMenu';

Expand All @@ -26,4 +27,4 @@ const Header = ({hasLogo = false, isNoTracingMode}: IProps) => (
</S.Header>
);

export default Header;
export default withCustomization(Header, 'header');
5 changes: 2 additions & 3 deletions web/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import * as S from './Layout.styled';
export type TCustomHeader = typeof Header;

interface IProps {
customHeader?: TCustomHeader;
hasMenu?: boolean;
}

Expand Down Expand Up @@ -52,7 +51,7 @@ const footerMenuItems = [
},
];

const Layout = ({hasMenu = false, customHeader: CustomHeader = Header}: IProps) => {
const Layout = ({hasMenu = false}: IProps) => {
useRouterSync();
const {dataStoreConfig, isLoading} = useSettingsValues();
const pathname = useLocation().pathname;
Expand Down Expand Up @@ -100,7 +99,7 @@ const Layout = ({hasMenu = false, customHeader: CustomHeader = Header}: IProps)
)}

<S.Layout>
<CustomHeader hasLogo={!hasMenu} isNoTracingMode={isNoTracingMode && !isLoading} />
<Header hasLogo={!hasMenu} isNoTracingMode={isNoTracingMode && !isLoading} />
<S.Content $hasMenu={hasMenu}>
<Outlet />
</S.Content>
Expand Down
12 changes: 4 additions & 8 deletions web/src/components/Router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,18 @@ import TestSuite from 'pages/TestSuite';
import TestSuiteRunOverview from 'pages/TestSuiteRunOverview';
import TestSuiteRunAutomate from 'pages/TestSuiteRunAutomate';
import AutomatedTestRun from 'pages/AutomatedTestRun';
import Layout, {TCustomHeader} from 'components/Layout/Layout';
import Layout from 'components/Layout/Layout';

interface IProps {
customHeader?: TCustomHeader;
}

const Router = ({customHeader}: IProps) => (
const Router = () => (
<Routes>
<Route element={<Layout hasMenu customHeader={customHeader} />}>
<Route element={<Layout hasMenu />}>
<Route path="/" element={<Home />} />
<Route path="/testsuites" element={<TestSuites />} />
<Route path="/variablesets" element={<VariableSet />} />
<Route path="/settings" element={<Settings />} />
</Route>

<Route element={<Layout customHeader={customHeader} />}>
<Route element={<Layout />}>
<Route path="/test/:testId" element={<Test />} />
<Route path="/test/:testId/run/:runId" element={<RunDetail />} />
<Route path="/test/:testId/run/:runId/:mode" element={<RunDetail />} />
Expand Down
11 changes: 9 additions & 2 deletions web/src/components/Settings/DataStoreForm/DataStoreForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Button, Form} from 'antd';
import {useCallback, useEffect, useMemo} from 'react';
import AllowButton, {Operation} from 'components/AllowButton';
import DataStoreService from 'services/DataStore.service';
import {TDraftDataStore, TDataStoreForm, SupportedDataStores} from 'types/DataStore.types';
import {SupportedDataStoresToName} from 'constants/DataStore.constants';
Expand Down Expand Up @@ -90,9 +91,15 @@ const DataStoreForm = ({
<Button loading={isTestConnectionLoading} type="primary" ghost onClick={onTestConnection}>
Test Connection
</Button>
<Button disabled={!isFormValid} loading={isLoading} type="primary" onClick={() => form.submit()}>
<AllowButton
operation={Operation.Configure}
disabled={!isFormValid}
loading={isLoading}
type="primary"
onClick={() => form.submit()}
>
Save and Set as DataStore
</Button>
</AllowButton>
</S.SaveContainer>
</S.ButtonsContainer>
</S.FactoryContainer>
Expand Down
30 changes: 30 additions & 0 deletions web/src/providers/Customization/Customization.provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {createContext, useContext} from 'react';

export enum Operation {
Configure = 'configure',
Edit = 'edit',
View = 'view',
}

interface IContext {
getComponent<T>(name: string, fallback: React.ComponentType<T>): React.ComponentType<T>;
getIsAllowed(operation: Operation): boolean;
}

export const Context = createContext<IContext>({
getComponent: (name, fallback) => fallback,
getIsAllowed: () => true,
});

export const useCustomization = () => useContext(Context);

interface IProps {
children: React.ReactNode;
value: IContext;
}

const CustomizationProvider = ({children, value}: IProps) => {
return <Context.Provider value={value}>{children}</Context.Provider>;
};

export default CustomizationProvider;
14 changes: 14 additions & 0 deletions web/src/providers/Customization/WithCustomization.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {useCustomization} from './Customization.provider';

const withCustomization = <P extends object>(Component: React.ComponentType<P>, id: string) => {
const WrappedComponent = (props: P) => {
const {getComponent} = useCustomization();
const CustomizedComponent = getComponent(id, Component);

return <CustomizedComponent {...props} />;
};

return WrappedComponent;
};

export default withCustomization;
3 changes: 3 additions & 0 deletions web/src/providers/Customization/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// eslint-disable-next-line no-restricted-exports
export {default, useCustomization, Operation} from './Customization.provider';
export {default as withCustomization} from './WithCustomization';