feat(AppConfig): iterate how apps are managed

- auth clients are always shared, cannot be disabled
- custom connections are enabled by default, can be disabled
- any existing connections can be reconnected regardless of its AppConfig or AppAuthClient states
This commit is contained in:
Ali BARIN
2024-12-09 17:46:51 +00:00
parent 17614d6d47
commit 2a77763c51
48 changed files with 192 additions and 563 deletions

View File

@@ -18,6 +18,7 @@ import { generateExternalLink } from 'helpers/translationValues';
import { Form } from './style';
import useAppAuth from 'hooks/useAppAuth';
import { useQueryClient } from '@tanstack/react-query';
import { useWhatChanged } from '@simbathesailor/use-what-changed';
function AddAppConnection(props) {
const { application, connectionId, onClose } = props;
@@ -64,7 +65,7 @@ function AddAppConnection(props) {
asyncAuthenticate();
},
[appAuthClientId, authenticate],
[appAuthClientId, authenticate, key, navigate],
);
const handleClientClick = (appAuthClientId) =>

View File

@@ -34,10 +34,10 @@ function AdminApplicationCreateAuthClient(props) {
if (!appConfigKey) {
const { data: appConfigData } = await createAppConfig({
customConnectionAllowed: true,
shared: false,
useOnlyPredefinedAuthClients: false,
disabled: false,
});
appConfigKey = appConfigData.key;
}

View File

@@ -46,9 +46,8 @@ function AdminApplicationSettings(props) {
const defaultValues = useMemo(
() => ({
customConnectionAllowed:
appConfig?.data?.customConnectionAllowed || false,
shared: appConfig?.data?.shared || false,
useOnlyPredefinedAuthClients:
appConfig?.data?.useOnlyPredefinedAuthClients || false,
disabled: appConfig?.data?.disabled || false,
}),
[appConfig?.data],
@@ -62,21 +61,17 @@ function AdminApplicationSettings(props) {
<Paper sx={{ p: 2, mt: 4 }}>
<Stack spacing={2} direction="column">
<Switch
name="customConnectionAllowed"
label={formatMessage('adminAppsSettings.customConnectionAllowed')}
FormControlLabelProps={{
labelPlacement: 'start',
}}
/>
<Divider />
<Switch
name="shared"
label={formatMessage('adminAppsSettings.shared')}
name="useOnlyPredefinedAuthClients"
label={formatMessage(
'adminAppsSettings.useOnlyPredefinedAuthClients',
)}
FormControlLabelProps={{
labelPlacement: 'start',
}}
/>
<Divider />
<Switch
name="disabled"
label={formatMessage('adminAppsSettings.disabled')}
@@ -86,6 +81,7 @@ function AdminApplicationSettings(props) {
/>
<Divider />
</Stack>
<Stack>
<LoadingButton
data-test="submit-button"

View File

@@ -15,17 +15,7 @@ function AppAuthClientsDialog(props) {
const formatMessage = useFormatMessage();
React.useEffect(
function autoAuthenticateSingleClient() {
if (appAuthClients?.data.length === 1) {
onClientClick(appAuthClients.data[0].id);
}
},
[appAuthClients?.data],
);
if (!appAuthClients?.data.length || appAuthClients?.data.length === 1)
return <React.Fragment />;
if (!appAuthClients?.data.length) return <React.Fragment />;
return (
<Dialog onClose={onClose} open={true}>

View File

@@ -11,14 +11,7 @@ import { useQueryClient } from '@tanstack/react-query';
import Can from 'components/Can';
function ContextMenu(props) {
const {
appKey,
connection,
onClose,
onMenuItemClick,
anchorEl,
disableReconnection,
} = props;
const { appKey, connection, onClose, onMenuItemClick, anchorEl } = props;
const formatMessage = useFormatMessage();
const queryClient = useQueryClient();
@@ -73,7 +66,7 @@ function ContextMenu(props) {
{(allowed) => (
<MenuItem
component={Link}
disabled={!allowed || disableReconnection}
disabled={!allowed}
to={URLS.APP_RECONNECT_CONNECTION(
appKey,
connection.id,
@@ -109,7 +102,6 @@ ContextMenu.propTypes = {
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
disableReconnection: PropTypes.bool.isRequired,
};
export default ContextMenu;

View File

@@ -30,8 +30,7 @@ const countTranslation = (value) => (
function AppConnectionRow(props) {
const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar();
const { id, key, formattedData, verified, createdAt, reconnectable } =
props.connection;
const { id, key, formattedData, verified, createdAt } = props.connection;
const [verificationVisible, setVerificationVisible] = React.useState(false);
const contextButtonRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
@@ -174,7 +173,6 @@ function AppConnectionRow(props) {
<ConnectionContextMenu
appKey={key}
connection={props.connection}
disableReconnection={!reconnectable}
onClose={handleClose}
onMenuItemClick={onContextMenuAction}
anchorEl={anchorEl}

View File

@@ -95,7 +95,8 @@ function ChooseConnectionSubstep(props) {
if (
!appConfig?.data ||
(!appConfig.data?.disabled && appConfig.data?.customConnectionAllowed)
(!appConfig.data?.disabled === false &&
appConfig.data?.useOnlyPredefinedAuthClients === false)
) {
options.push({
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
@@ -103,12 +104,10 @@ function ChooseConnectionSubstep(props) {
});
}
if (appConfig?.data?.connectionAllowed) {
options.push({
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
value: ADD_SHARED_CONNECTION_VALUE,
});
}
options.push({
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
value: ADD_SHARED_CONNECTION_VALUE,
});
return options;
}, [data, formatMessage, appConfig?.data]);

View File

@@ -67,17 +67,12 @@ export default function SplitButton(props) {
}}
open={open}
anchorEl={anchorRef.current}
placement="bottom-end"
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem>

View File

@@ -13,6 +13,7 @@ import useCreateConnectionAuthUrl from './useCreateConnectionAuthUrl';
import useUpdateConnection from './useUpdateConnection';
import useResetConnection from './useResetConnection';
import useVerifyConnection from './useVerifyConnection';
import { useWhatChanged } from '@simbathesailor/use-what-changed';
function getSteps(auth, hasConnection, useShared) {
if (hasConnection) {
@@ -37,11 +38,13 @@ export default function useAuthenticateApp(payload) {
const { mutateAsync: createConnectionAuthUrl } = useCreateConnectionAuthUrl();
const { mutateAsync: updateConnection } = useUpdateConnection();
const { mutateAsync: resetConnection } = useResetConnection();
const { mutateAsync: verifyConnection } = useVerifyConnection();
const [authenticationInProgress, setAuthenticationInProgress] =
React.useState(false);
const formatMessage = useFormatMessage();
const steps = getSteps(auth?.data, !!connectionId, useShared);
const { mutateAsync: verifyConnection } = useVerifyConnection();
const steps = React.useMemo(() => {
return getSteps(auth?.data, !!connectionId, useShared);
}, [auth, connectionId, useShared]);
const authenticate = React.useMemo(() => {
if (!steps?.length) return;
@@ -57,7 +60,6 @@ export default function useAuthenticateApp(payload) {
fields,
};
let stepIndex = 0;
while (stepIndex < steps?.length) {
const step = steps[stepIndex];
const variables = computeAuthStepVariables(step.arguments, response);
@@ -105,10 +107,10 @@ export default function useAuthenticateApp(payload) {
response[step.name] = stepResponse;
}
} catch (err) {
console.log(err);
console.error(err);
setAuthenticationInProgress(false);
queryClient.invalidateQueries({
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'connections'],
});
@@ -126,13 +128,14 @@ export default function useAuthenticateApp(payload) {
return response;
};
// keep formatMessage out of it as it causes infinite loop.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
steps,
appKey,
appAuthClientId,
connectionId,
queryClient,
formatMessage,
createConnection,
createConnectionAuthUrl,
updateConnection,
@@ -140,6 +143,24 @@ export default function useAuthenticateApp(payload) {
verifyConnection,
]);
useWhatChanged(
[
steps,
appKey,
appAuthClientId,
connectionId,
queryClient,
createConnection,
createConnectionAuthUrl,
updateConnection,
resetConnection,
verifyConnection,
],
'steps, appKey, appAuthClientId, connectionId, queryClient, createConnection, createConnectionAuthUrl, updateConnection, resetConnection, verifyConnection',
'',
'useAuthenticate',
);
return {
authenticate,
inProgress: authenticationInProgress,

View File

@@ -9,7 +9,7 @@ export default function useAutomatischInfo() {
**/
staleTime: Infinity,
queryKey: ['automatisch', 'info'],
queryFn: async (payload, signal) => {
queryFn: async ({ signal }) => {
const { data } = await api.get('/v1/automatisch/info', { signal });
return data;

View File

@@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useCreateConnection(appKey) {
const query = useMutation({
const mutation = useMutation({
mutationFn: async ({ appAuthClientId, formattedData }) => {
const { data } = await api.post(`/v1/apps/${appKey}/connections`, {
appAuthClientId,
@@ -14,5 +14,5 @@ export default function useCreateConnection(appKey) {
},
});
return query;
return mutation;
}

View File

@@ -0,0 +1,15 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useLicense() {
const query = useQuery({
queryKey: ['automatisch', 'license'],
queryFn: async ({ signal }) => {
const { data } = await api.get('/v1/automatisch/license', { signal });
return data;
},
});
return query;
}

View File

@@ -1,5 +1,6 @@
import { createRoot } from 'react-dom/client';
import { Settings } from 'luxon';
import { setUseWhatChange } from '@simbathesailor/use-what-changed';
import ThemeProvider from 'components/ThemeProvider';
import IntlProvider from 'components/IntlProvider';
@@ -14,6 +15,8 @@ import reportWebVitals from './reportWebVitals';
// Sets the default locale to English for all luxon DateTime instances created afterwards.
Settings.defaultLocale = 'en';
setUseWhatChange(process.env.NODE_ENV === 'development');
const container = document.getElementById('root');
const root = createRoot(container);

View File

@@ -22,7 +22,7 @@
"app.connectionCount": "{count} connections",
"app.flowCount": "{count} flows",
"app.addConnection": "Add connection",
"app.addCustomConnection": "Add custom connection",
"app.addConnectionWithAuthClient": "Add connection with auth client",
"app.reconnectConnection": "Reconnect connection",
"app.createFlow": "Create flow",
"app.settings": "Settings",
@@ -292,7 +292,7 @@
"adminApps.connections": "Connections",
"adminApps.authClients": "Auth clients",
"adminApps.settings": "Settings",
"adminAppsSettings.customConnectionAllowed": "Allow custom connection",
"adminAppsSettings.useOnlyPredefinedAuthClients": "Use only predefined auth clients",
"adminAppsSettings.shared": "Shared",
"adminAppsSettings.disabled": "Disabled",
"adminAppsSettings.save": "Save",

View File

@@ -6,7 +6,6 @@ import {
Navigate,
Routes,
useParams,
useSearchParams,
useMatch,
useNavigate,
} from 'react-router-dom';
@@ -31,6 +30,7 @@ import AppIcon from 'components/AppIcon';
import Container from 'components/Container';
import PageTitle from 'components/PageTitle';
import useApp from 'hooks/useApp';
import useAppAuthClients from 'hooks/useAppAuthClients';
import Can from 'components/Can';
import { AppPropType } from 'propTypes/propTypes';
@@ -61,47 +61,53 @@ export default function Application() {
end: false,
});
const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false });
const [searchParams] = useSearchParams();
const { appKey } = useParams();
const navigate = useNavigate();
const { data: appAuthClients } = useAppAuthClients(appKey);
const { data, loading } = useApp(appKey);
const app = data?.data || {};
const { data: appConfig } = useAppConfig(appKey);
const connectionId = searchParams.get('connectionId') || undefined;
const currentUserAbility = useCurrentUserAbility();
const goToApplicationPage = () => navigate('connections');
const connectionOptions = React.useMemo(() => {
const shouldHaveCustomConnection =
appConfig?.data?.connectionAllowed &&
appConfig?.data?.customConnectionAllowed;
const addCustomConnection = {
label: formatMessage('app.addConnection'),
key: 'addConnection',
'data-test': 'add-connection-button',
to: URLS.APP_ADD_CONNECTION(appKey, false),
disabled: !currentUserAbility.can('create', 'Connection'),
};
const options = [
{
label: formatMessage('app.addConnection'),
key: 'addConnection',
'data-test': 'add-connection-button',
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.data?.connectionAllowed),
disabled: !currentUserAbility.can('create', 'Connection'),
},
];
const addConnectionWithAuthClient = {
label: formatMessage('app.addConnectionWithAuthClient'),
key: 'addConnectionWithAuthClient',
'data-test': 'add-custom-connection-button',
to: URLS.APP_ADD_CONNECTION(appKey, true),
disabled: !currentUserAbility.can('create', 'Connection'),
};
if (shouldHaveCustomConnection) {
options.push({
label: formatMessage('app.addCustomConnection'),
key: 'addCustomConnection',
'data-test': 'add-custom-connection-button',
to: URLS.APP_ADD_CONNECTION(appKey),
disabled: !currentUserAbility.can('create', 'Connection'),
});
// means there is no app config. defaulting to custom connections only
if (!appConfig?.data) {
return [addCustomConnection];
}
return options;
}, [appKey, appConfig?.data, currentUserAbility, formatMessage]);
// means there is no app auth client. so we don't show the `addConnectionWithAuthClient`
if (appAuthClients?.data?.length === 0) {
return [addCustomConnection];
}
// means only auth clients are allowed for connection creation
if (appConfig?.data?.useOnlyPredefinedAuthClients === true) {
return [addConnectionWithAuthClient];
}
return [addCustomConnection, addConnectionWithAuthClient];
}, [appKey, appConfig, appAuthClients, currentUserAbility, formatMessage]);
if (loading) return null;
@@ -154,12 +160,7 @@ export default function Application() {
{(allowed) => (
<SplitButton
disabled={
!allowed ||
(appConfig?.data &&
!appConfig?.data?.disabled &&
!appConfig?.data?.connectionAllowed &&
!appConfig?.data?.customConnectionAllowed) ||
connectionOptions.every(({ disabled }) => disabled)
!allowed || appConfig?.data?.disabled === true
}
options={connectionOptions}
/>

View File

@@ -211,7 +211,6 @@ export const ConnectionPropType = PropTypes.shape({
flowCount: PropTypes.number,
appData: AppPropType,
createdAt: PropTypes.number,
reconnectable: PropTypes.bool,
appAuthClientId: PropTypes.string,
});
@@ -459,8 +458,7 @@ export const SamlAuthProviderRolePropType = PropTypes.shape({
export const AppConfigPropType = PropTypes.shape({
id: PropTypes.string,
key: PropTypes.string,
customConnectionAllowed: PropTypes.bool,
connectionAllowed: PropTypes.bool,
useOnlyPredefinedAuthClients: PropTypes.bool,
shared: PropTypes.bool,
disabled: PropTypes.bool,
});