Compare commits

..

1 Commits

Author SHA1 Message Date
kasia.oczkowska
b48b2592d5 feat: show api error message when logging in fails 2024-11-13 14:46:21 +00:00
23 changed files with 2150 additions and 1815 deletions

View File

@@ -34,6 +34,7 @@ cd $CURRENT_DIR
echo "Installing and linking dependencies..." echo "Installing and linking dependencies..."
yarn yarn
yarn lerna bootstrap
echo "Migrating database..." echo "Migrating database..."
cd packages/backend cd packages/backend

View File

@@ -38,7 +38,7 @@ jobs:
cache-dependency-path: yarn.lock cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start - run: cd packages/backend && yarn start
env: env:
ENCRYPTION_KEY: sample_encryption_key ENCRYPTION_KEY: sample_encryption_key
@@ -58,7 +58,7 @@ jobs:
cache-dependency-path: yarn.lock cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start:worker - run: cd packages/backend && yarn start:worker
env: env:
ENCRYPTION_KEY: sample_encryption_key ENCRYPTION_KEY: sample_encryption_key
@@ -78,7 +78,7 @@ jobs:
cache-dependency-path: yarn.lock cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/web && yarn build - run: cd packages/web && yarn build
env: env:
CI: false CI: false

View File

@@ -59,7 +59,7 @@ jobs:
with: with:
node-version: 18 node-version: 18
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn && yarn lerna bootstrap
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
- name: Build Automatisch web - name: Build Automatisch web

View File

@@ -1,11 +1,13 @@
{ {
"packages": ["packages/*"], "packages": [
"version": "0.14.0", "packages/*"
],
"version": "0.10.0",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true,
"command": { "command": {
"add": { "add": {
"exact": true "exact": true
} }
}, }
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
} }

View File

@@ -23,7 +23,7 @@
"eslint": "^8.13.0", "eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"lerna": "^8.0.0", "lerna": "^4.0.0",
"prettier": "^2.5.1" "prettier": "^2.5.1"
}, },
"publishConfig": { "publishConfig": {

View File

@@ -8,7 +8,7 @@ export default {
key: 'instanceUrl', key: 'instanceUrl',
label: 'WordPress instance URL', label: 'WordPress instance URL',
type: 'string', type: 'string',
required: true, required: false,
readOnly: false, readOnly: false,
value: null, value: null,
placeholder: null, placeholder: null,

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development', isDev: appEnv === 'development',
isTest: appEnv === 'test', isTest: appEnv === 'test',
isProd: appEnv === 'production', isProd: appEnv === 'production',
version: '0.14.0', version: '0.13.1',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development', postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public', postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = { const expectedPayload = {
data: { data: {
version: '0.14.0', version: '0.13.1',
}, },
meta: { meta: {
count: 1, count: 1,

View File

@@ -1,9 +1,8 @@
import { vi, describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import SamlAuthProvider from '../models/saml-auth-provider.ee'; import SamlAuthProvider from '../models/saml-auth-provider.ee';
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee'; import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
import Identity from './identity.ee'; import Identity from './identity.ee';
import Base from './base'; import Base from './base';
import appConfig from '../config/app';
describe('SamlAuthProvider model', () => { describe('SamlAuthProvider model', () => {
it('tableName should return correct name', () => { it('tableName should return correct name', () => {
@@ -46,39 +45,4 @@ describe('SamlAuthProvider model', () => {
expect(virtualAttributes).toStrictEqual(expectedAttributes); expect(virtualAttributes).toStrictEqual(expectedAttributes);
}); });
it('loginUrl should return the URL of login', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(samlAuthProvider.loginUrl).toStrictEqual(
'https://automatisch.io/login/saml/sample-issuer'
);
});
it('loginCallbackUrl should return the URL of login callback', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(samlAuthProvider.loginCallBackUrl).toStrictEqual(
'https://automatisch.io/login/saml/sample-issuer/callback'
);
});
it('remoteLogoutUrl should return the URL from entrypoint', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.entryPoint = 'https://example.com/saml/logout';
expect(samlAuthProvider.remoteLogoutUrl).toStrictEqual(
'https://example.com/saml/logout'
);
});
}); });

View File

@@ -580,28 +580,4 @@ describe('User model', () => {
expect(refetchedUser.invitationTokenSentAt).toBe(null); expect(refetchedUser.invitationTokenSentAt).toBe(null);
expect(refetchedUser.status).toBe('active'); expect(refetchedUser.status).toBe('active');
}); });
describe('updatePassword', () => {
it('should update password when the given current password matches with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
const updatedUser = await user.updatePassword({
currentPassword: 'sample-password',
password: 'new-password',
});
expect(await updatedUser.login('new-password')).toBe(true);
});
it('should throw validation error when the given current password does not match with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
await expect(
user.updatePassword({
currentPassword: 'wrong-password',
password: 'new-password',
})
).rejects.toThrowError('currentPassword: is incorrect.');
});
});
}); });

View File

@@ -112,7 +112,7 @@ export default function ResetPasswordForm() {
<Alert <Alert
data-test="accept-invitation-form-error" data-test="accept-invitation-form-error"
severity="error" severity="error"
sx={{ mt: 1 }} sx={{ mt: 1, fontWeight: 500 }}
> >
{formatMessage('acceptInvitationForm.invalidToken')} {formatMessage('acceptInvitationForm.invalidToken')}
</Alert> </Alert>

View File

@@ -126,7 +126,7 @@ function AddAppConnection(props) {
</DialogTitle> </DialogTitle>
{authDocUrl && ( {authDocUrl && (
<Alert severity="info"> <Alert severity="info" sx={{ fontWeight: 300 }}>
{formatMessage('addAppConnection.callToDocs', { {formatMessage('addAppConnection.callToDocs', {
appName: name, appName: name,
docsLink: generateExternalLink(authDocUrl), docsLink: generateExternalLink(authDocUrl),
@@ -138,7 +138,7 @@ function AddAppConnection(props) {
<Alert <Alert
data-test="add-connection-error" data-test="add-connection-error"
severity="error" severity="error"
sx={{ mt: 1, wordBreak: 'break-all' }} sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
> >
{!errorDetails && errorMessage} {!errorDetails && errorMessage}
{errorDetails && ( {errorDetails && (

View File

@@ -32,7 +32,10 @@ function AdminApplicationAuthClientDialog(props) {
<Dialog open={true} onClose={onClose}> <Dialog open={true} onClose={onClose}>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
{error && ( {error && (
<Alert severity="error" sx={{ mt: 1, wordBreak: 'break-all' }}> <Alert
severity="error"
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{error.message} {error.message}
</Alert> </Alert>
)} )}

View File

@@ -188,7 +188,7 @@ function InstallationForm() {
)} )}
/> />
{install.isSuccess && ( {install.isSuccess && (
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}> <Alert data-test="success-alert" severity="success" sx={{ mt: 3, fontWeight: 500 }}>
{formatMessage('installationForm.success', { {formatMessage('installationForm.success', {
link: (str) => ( link: (str) => (
<Link <Link

View File

@@ -11,19 +11,16 @@ import Form from 'components/Form';
import TextField from 'components/TextField'; import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useCreateAccessToken from 'hooks/useCreateAccessToken'; import useCreateAccessToken from 'hooks/useCreateAccessToken';
import { Alert } from '@mui/material'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
function LoginForm() { function LoginForm() {
const isCloud = useCloud(); const isCloud = useCloud();
const navigate = useNavigate(); const navigate = useNavigate();
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar();
const authentication = useAuthentication(); const authentication = useAuthentication();
const { const { mutateAsync: createAccessToken, isPending: loading } =
mutateAsync: createAccessToken, useCreateAccessToken();
isPending: loading,
error,
isError,
} = useCreateAccessToken();
React.useEffect(() => { React.useEffect(() => {
if (authentication.isAuthenticated) { if (authentication.isAuthenticated) {
@@ -40,19 +37,23 @@ function LoginForm() {
}); });
const { token } = data; const { token } = data;
authentication.updateToken(token); authentication.updateToken(token);
} catch {} } catch (error) {
}; const errors = error?.response?.data?.errors
? Object.values(error.response.data.errors)
: [];
const renderError = () => { if (errors.length) {
const errors = error?.response?.data?.errors?.general || [ for (const [error] of errors) {
formatMessage('loginForm.error'), enqueueSnackbar(error, {
]; variant: 'error',
});
return errors.map((error) => ( }
<Alert severity="error" sx={{ mt: 2 }}> } else {
{error} enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
</Alert> variant: 'error',
)); });
}
}
}; };
return ( return (
@@ -105,8 +106,6 @@ function LoginForm() {
</Link> </Link>
)} )}
{isError && renderError()}
<LoadingButton <LoadingButton
type="submit" type="submit"
variant="contained" variant="contained"

View File

@@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
export default function SearchInput({ onChange, defaultValue = '' }) { export default function SearchInput({ onChange }) {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
return ( return (
<FormControl variant="outlined" fullWidth> <FormControl variant="outlined" fullWidth>
@@ -16,7 +16,6 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
</InputLabel> </InputLabel>
<OutlinedInput <OutlinedInput
defaultValue={defaultValue}
id="search-input" id="search-input"
type="text" type="text"
size="medium" size="medium"
@@ -35,5 +34,4 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
SearchInput.propTypes = { SearchInput.propTypes = {
onChange: PropTypes.func, onChange: PropTypes.func,
defaultValue: PropTypes.string,
}; };

View File

@@ -84,7 +84,10 @@ function TestSubstep(props) {
}} }}
> >
{hasError && ( {hasError && (
<Alert severity="error" sx={{ mb: 2, width: '100%' }}> <Alert
severity="error"
sx={{ mb: 2, fontWeight: 500, width: '100%' }}
>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}> <pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(errorDetails, null, 2)} {JSON.stringify(errorDetails, null, 2)}
</pre> </pre>
@@ -101,11 +104,13 @@ function TestSubstep(props) {
severity="warning" severity="warning"
sx={{ mb: 1, width: '100%' }} sx={{ mb: 1, width: '100%' }}
> >
<AlertTitle> <AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('flowEditor.noTestDataTitle')} {formatMessage('flowEditor.noTestDataTitle')}
</AlertTitle> </AlertTitle>
<Box>{formatMessage('flowEditor.noTestDataMessage')}</Box> <Box sx={{ fontWeight: 400 }}>
{formatMessage('flowEditor.noTestDataMessage')}
</Box>
</Alert> </Alert>
)} )}

View File

@@ -124,6 +124,7 @@ export default function CreateUser() {
<Alert <Alert
severity="info" severity="info"
color="primary" color="primary"
sx={{ fontWeight: '500' }}
data-test="invitation-email-info-alert" data-test="invitation-email-info-alert"
> >
{formatMessage('createUser.invitationEmailInfo', { {formatMessage('createUser.invitationEmailInfo', {

View File

@@ -42,9 +42,13 @@ export default function Execution() {
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}> <Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && ( {!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
<Alert severity="warning" sx={{ flex: 1 }}> <Alert severity="warning" sx={{ flex: 1 }}>
<AlertTitle>{formatMessage('execution.noDataTitle')}</AlertTitle> <AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('execution.noDataTitle')}
</AlertTitle>
<Box>{formatMessage('execution.noDataMessage')}</Box> <Box sx={{ fontWeight: 400 }}>
{formatMessage('execution.noDataMessage')}
</Box>
</Alert> </Alert>
)} )}

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
@@ -23,18 +23,13 @@ import useLazyFlows from 'hooks/useLazyFlows';
export default function Flows() { export default function Flows() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const page = parseInt(searchParams.get('page') || '', 10) || 1; const page = parseInt(searchParams.get('page') || '', 10) || 1;
const flowName = searchParams.get('flowName') || ''; const [flowName, setFlowName] = React.useState('');
const [isLoading, setIsLoading] = React.useState(true); const [isLoading, setIsLoading] = React.useState(false);
const currentUserAbility = useCurrentUserAbility(); const currentUserAbility = useCurrentUserAbility();
const { const { data, mutate: fetchFlows } = useLazyFlows(
data,
mutate: fetchFlows,
isSuccess,
} = useLazyFlows(
{ flowName, page }, { flowName, page },
{ {
onSettled: () => { onSettled: () => {
@@ -43,36 +38,6 @@ export default function Flows() {
}, },
); );
const flows = data?.data || [];
const pageInfo = data?.meta;
const hasFlows = flows?.length;
const navigateToLastPage = isSuccess && !hasFlows && page > 1;
const onSearchChange = React.useCallback((event) => {
setSearchParams({ flowName: event.target.value });
}, []);
const getPathWithSearchParams = (page, flowName) => {
const searchParams = new URLSearchParams();
if (page > 1) {
searchParams.set('page', page);
}
if (flowName) {
searchParams.set('flowName', flowName);
}
return { search: searchParams.toString() };
};
const onDuplicateFlow = () => {
if (pageInfo?.currentPage > 1) {
navigate(getPathWithSearchParams(1, flowName));
} else {
fetchFlows();
}
};
const fetchData = React.useMemo( const fetchData = React.useMemo(
() => debounce(fetchFlows, 300), () => debounce(fetchFlows, 300),
[fetchFlows], [fetchFlows],
@@ -89,14 +54,21 @@ export default function Flows() {
}, [fetchData, flowName, page]); }, [fetchData, flowName, page]);
React.useEffect( React.useEffect(
function redirectToLastPage() { function resetPageOnSearch() {
if (navigateToLastPage) { // reset search params which only consists of `page`
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName)); setSearchParams({});
}
}, },
[navigateToLastPage], [flowName],
); );
const flows = data?.data || [];
const pageInfo = data?.meta;
const hasFlows = flows?.length;
const onSearchChange = React.useCallback((event) => {
setFlowName(event.target.value);
}, []);
return ( return (
<Box sx={{ py: 3 }}> <Box sx={{ py: 3 }}>
<Container> <Container>
@@ -106,7 +78,7 @@ export default function Flows() {
</Grid> </Grid>
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}> <Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
<SearchInput onChange={onSearchChange} defaultValue={flowName} /> <SearchInput onChange={onSearchChange} />
</Grid> </Grid>
<Grid <Grid
@@ -139,7 +111,7 @@ export default function Flows() {
</Grid> </Grid>
<Divider sx={{ mt: [2, 0], mb: 2 }} /> <Divider sx={{ mt: [2, 0], mb: 2 }} />
{(isLoading || navigateToLastPage) && ( {isLoading && (
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} /> <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
)} )}
{!isLoading && {!isLoading &&
@@ -147,11 +119,11 @@ export default function Flows() {
<FlowRow <FlowRow
key={flow.id} key={flow.id}
flow={flow} flow={flow}
onDuplicateFlow={onDuplicateFlow} onDuplicateFlow={fetchFlows}
onDeleteFlow={fetchFlows} onDeleteFlow={fetchFlows}
/> />
))} ))}
{!isLoading && !navigateToLastPage && !hasFlows && ( {!isLoading && !hasFlows && (
<NoResultFound <NoResultFound
text={formatMessage('flows.noFlows')} text={formatMessage('flows.noFlows')}
{...(currentUserAbility.can('create', 'Flow') && { {...(currentUserAbility.can('create', 'Flow') && {
@@ -159,23 +131,23 @@ export default function Flows() {
})} })}
/> />
)} )}
{!isLoading && {!isLoading && pageInfo && pageInfo.totalPages > 1 && (
!navigateToLastPage && <Pagination
pageInfo && sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
pageInfo.totalPages > 1 && ( page={pageInfo?.currentPage}
<Pagination count={pageInfo?.totalPages}
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }} onChange={(event, page) =>
page={pageInfo?.currentPage} setSearchParams({ page: page.toString() })
count={pageInfo?.totalPages} }
renderItem={(item) => ( renderItem={(item) => (
<PaginationItem <PaginationItem
component={Link} component={Link}
to={getPathWithSearchParams(item.page, flowName)} to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
{...item} {...item}
/> />
)} )}
/> />
)} )}
</Container> </Container>
</Box> </Box>
); );

View File

@@ -266,8 +266,8 @@ function ProfileSettings() {
</Grid> </Grid>
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}> <Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
<Alert variant="outlined" severity="error"> <Alert variant="outlined" severity="error" sx={{ fontWeight: 500 }}>
<AlertTitle> <AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('profileSettings.deleteMyAccount')} {formatMessage('profileSettings.deleteMyAccount')}
</AlertTitle> </AlertTitle>

View File

@@ -278,20 +278,6 @@ export const defaultTheme = createTheme({
}), }),
}, },
}, },
MuiAlert: {
styleOverrides: {
root: ({ theme }) => ({
fontWeight: theme.typography.fontWeightRegular,
}),
},
},
MuiAlertTitle: {
styleOverrides: {
root: ({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
}),
},
},
}, },
}); });
export const mationTheme = createTheme( export const mationTheme = createTheme(

3678
yarn.lock

File diff suppressed because it is too large Load Diff