Skip to content

Commit

Permalink
feature(frontend): Adding customizable logic (#3112)
Browse files Browse the repository at this point in the history
* feature(frontend): Adding customizable logic

* feature(frontend): Adding customizable logic

* update naming and exports

---------

Co-authored-by: Jorge Padilla <jorge.esteban.padilla@gmail.com>
  • Loading branch information
xoscar and jorgeepc committed Aug 31, 2023
1 parent ac53793 commit a906404
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 25 deletions.
11 changes: 7 additions & 4 deletions web/src/App.tsx
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
// eslint-disable-next-line no-restricted-exports
export {default, useCustomization, Operation} from './Customization.provider';
export {default as withCustomization} from './WithCustomization';

0 comments on commit a906404

Please sign in to comment.