Merge pull request #988 from automatisch/show-alert-for-task-usage
feat: show usage alert as of threshold
This commit is contained in:
@@ -12,6 +12,7 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
|||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import AccountDropdownMenu from 'components/AccountDropdownMenu';
|
import AccountDropdownMenu from 'components/AccountDropdownMenu';
|
||||||
|
import UsageAlert from 'components/UsageAlert/index.ee';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { Link } from './style';
|
import { Link } from './style';
|
||||||
@@ -29,9 +30,7 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
|
|||||||
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
|
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
noSsr: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
|
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
|
||||||
React.useState<null | HTMLElement>(null);
|
React.useState<null | HTMLElement>(null);
|
||||||
@@ -83,6 +82,8 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
|
|||||||
</Toolbar>
|
</Toolbar>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
<UsageAlert />
|
||||||
|
|
||||||
<AccountDropdownMenu
|
<AccountDropdownMenu
|
||||||
anchorEl={accountMenuAnchorElement}
|
anchorEl={accountMenuAnchorElement}
|
||||||
id={accountMenuId}
|
id={accountMenuId}
|
||||||
|
@@ -10,9 +10,7 @@ import { IconButton } from './style';
|
|||||||
export default function ConditionalIconButton(props: any): React.ReactElement {
|
export default function ConditionalIconButton(props: any): React.ReactElement {
|
||||||
const { icon, ...buttonProps } = props;
|
const { icon, ...buttonProps } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
noSsr: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matchSmallScreens) {
|
if (matchSmallScreens) {
|
||||||
return (
|
return (
|
||||||
|
@@ -31,9 +31,7 @@ type DrawerProps = {
|
|||||||
export default function Drawer(props: DrawerProps): React.ReactElement {
|
export default function Drawer(props: DrawerProps): React.ReactElement {
|
||||||
const { links = [], bottomLinks = [], ...drawerProps } = props;
|
const { links = [], bottomLinks = [], ...drawerProps } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
noSsr: true,
|
|
||||||
});
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const closeOnClick = (event: React.SyntheticEvent) => {
|
const closeOnClick = (event: React.SyntheticEvent) => {
|
||||||
|
@@ -52,9 +52,7 @@ export default function PublicLayout({
|
|||||||
}: PublicLayoutProps): React.ReactElement {
|
}: PublicLayoutProps): React.ReactElement {
|
||||||
const version = useVersion();
|
const version = useVersion();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), {
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
noSsr: true,
|
|
||||||
});
|
|
||||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||||
|
|
||||||
const openDrawer = () => setDrawerOpen(true);
|
const openDrawer = () => setDrawerOpen(true);
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
|
|
||||||
import appConfig from 'config/app';
|
import appConfig from 'config/app';
|
||||||
import useCurrentUser from 'hooks/useCurrentUser';
|
import useCurrentUser from 'hooks/useCurrentUser';
|
||||||
@@ -8,7 +10,9 @@ type ChatwootProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Chatwoot = ({ ready }: ChatwootProps) => {
|
const Chatwoot = ({ ready }: ChatwootProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
React.useEffect(function initiateChatwoot() {
|
React.useEffect(function initiateChatwoot() {
|
||||||
window.chatwootSDK.run({
|
window.chatwootSDK.run({
|
||||||
@@ -30,8 +34,18 @@ const Chatwoot = ({ ready }: ChatwootProps) => {
|
|||||||
name: currentUser.fullName,
|
name: currentUser.fullName,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.$chatwoot.toggleBubbleVisibility("show");
|
if (!matchSmallScreens) {
|
||||||
}, [currentUser, ready]);
|
window.$chatwoot.toggleBubbleVisibility("show");
|
||||||
|
}
|
||||||
|
}, [currentUser, ready, matchSmallScreens]);
|
||||||
|
|
||||||
|
React.useLayoutEffect(function hideChatwoot() {
|
||||||
|
if (matchSmallScreens) {
|
||||||
|
window.$chatwoot?.toggleBubbleVisibility('hide');
|
||||||
|
} else {
|
||||||
|
window.$chatwoot?.toggleBubbleVisibility('show');
|
||||||
|
}
|
||||||
|
}, [matchSmallScreens])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment />
|
<React.Fragment />
|
||||||
|
@@ -49,9 +49,7 @@ export default function SettingsLayout({
|
|||||||
}: SettingsLayoutProps): React.ReactElement {
|
}: SettingsLayoutProps): React.ReactElement {
|
||||||
const { isCloud } = useAutomatischInfo();
|
const { isCloud } = useAutomatischInfo();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), {
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
noSsr: true,
|
|
||||||
});
|
|
||||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||||
|
|
||||||
const openDrawer = () => setDrawerOpen(true);
|
const openDrawer = () => setDrawerOpen(true);
|
||||||
|
57
packages/web/src/components/UsageAlert/index.ee.tsx
Normal file
57
packages/web/src/components/UsageAlert/index.ee.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useUsageAlert from 'hooks/useUsageAlert.ee';
|
||||||
|
|
||||||
|
const LinkBehavior = React.forwardRef<any>(
|
||||||
|
(props, ref) => <a ref={ref} target="_blank" {...props} role={undefined} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function UsageAlert() {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const usageAlert = useUsageAlert();
|
||||||
|
|
||||||
|
if (!usageAlert.showAlert) return (<React.Fragment />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
icon={false}
|
||||||
|
sx={{ fontWeight: 500, minWidth: 410 }}
|
||||||
|
severity={usageAlert.hasExceededLimit ? 'error' : 'warning'}
|
||||||
|
>
|
||||||
|
<Stack direction="row" gap={4} mb={1}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{ display: 'flex', alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
{usageAlert.alertMessage}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
component={LinkBehavior}
|
||||||
|
size="small"
|
||||||
|
href={usageAlert.url}
|
||||||
|
sx={{ minWidth: 100 }}
|
||||||
|
>
|
||||||
|
{formatMessage('usageAlert.viewPlans')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={usageAlert.consumptionPercentage}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
);
|
||||||
|
}
|
@@ -7,6 +7,7 @@ const config: Config = {
|
|||||||
graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string,
|
graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string,
|
||||||
notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string,
|
notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string,
|
||||||
chatwootBaseUrl: 'https://app.chatwoot.com',
|
chatwootBaseUrl: 'https://app.chatwoot.com',
|
||||||
|
supportEmailAddress: 'support@automatisch.io'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
46
packages/web/src/hooks/useUsageAlert.ee.ts
Normal file
46
packages/web/src/hooks/useUsageAlert.ee.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import useFormatMessage from './useFormatMessage';
|
||||||
|
import useUsageData from './useUsageData.ee';
|
||||||
|
import usePaymentPortalUrl from './usePaymentPortalUrl.ee';
|
||||||
|
|
||||||
|
type UseUsageAlertReturn = {
|
||||||
|
showAlert: boolean;
|
||||||
|
hasExceededLimit?: boolean;
|
||||||
|
alertMessage?: string;
|
||||||
|
url?: string;
|
||||||
|
consumptionPercentage?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useUsageAlert(): UseUsageAlertReturn {
|
||||||
|
const { url, loading: paymentPortalUrlLoading } = usePaymentPortalUrl();
|
||||||
|
const {
|
||||||
|
allowedTaskCount,
|
||||||
|
consumedTaskCount,
|
||||||
|
nextResetAt,
|
||||||
|
loading: usageDataLoading
|
||||||
|
} = useUsageData();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
if (paymentPortalUrlLoading || usageDataLoading) {
|
||||||
|
return { showAlert: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLoaded = !paymentPortalUrlLoading || usageDataLoading;
|
||||||
|
const withinUsageThreshold = consumedTaskCount > allowedTaskCount * 0.7;
|
||||||
|
const consumptionPercentage = consumedTaskCount / allowedTaskCount * 100;
|
||||||
|
const showAlert = hasLoaded && withinUsageThreshold;
|
||||||
|
const hasExceededLimit = consumedTaskCount >= allowedTaskCount;
|
||||||
|
|
||||||
|
const alertMessage = formatMessage('usageAlert.informationText', {
|
||||||
|
allowedTaskCount,
|
||||||
|
consumedTaskCount,
|
||||||
|
relativeResetDate: nextResetAt?.toRelative(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
showAlert,
|
||||||
|
hasExceededLimit,
|
||||||
|
alertMessage,
|
||||||
|
consumptionPercentage,
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
}
|
@@ -136,5 +136,7 @@
|
|||||||
"resetPasswordForm.submit": "Reset password",
|
"resetPasswordForm.submit": "Reset password",
|
||||||
"resetPasswordForm.passwordFieldLabel": "Password",
|
"resetPasswordForm.passwordFieldLabel": "Password",
|
||||||
"resetPasswordForm.confirmPasswordFieldLabel": "Confirm password",
|
"resetPasswordForm.confirmPasswordFieldLabel": "Confirm password",
|
||||||
"resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login."
|
"resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.",
|
||||||
}
|
"usageAlert.informationText": "Tasks: {consumedTaskCount}/{allowedTaskCount} (Resets {relativeResetDate})",
|
||||||
|
"usageAlert.viewPlans": "View plans"
|
||||||
|
}
|
@@ -51,9 +51,7 @@ const ReconnectConnection = (props: any): React.ReactElement => {
|
|||||||
|
|
||||||
export default function Application(): React.ReactElement | null {
|
export default function Application(): React.ReactElement | null {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
noSsr: true,
|
|
||||||
});
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const connectionsPathMatch = useMatch({
|
const connectionsPathMatch = useMatch({
|
||||||
path: URLS.APP_CONNECTIONS_PATTERN,
|
path: URLS.APP_CONNECTIONS_PATTERN,
|
||||||
|
@@ -251,6 +251,11 @@ const extendedTheme = createTheme({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiUseMediaQuery: {
|
||||||
|
defaultProps: {
|
||||||
|
noSsr: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiTab: {
|
MuiTab: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: ({ theme }) => ({
|
root: ({ theme }) => ({
|
||||||
|
Reference in New Issue
Block a user