feat: style app connections

This commit is contained in:
Ali BARIN
2021-12-15 21:58:14 +01:00
parent fc85716d07
commit 78375934d7
17 changed files with 232 additions and 34 deletions

View File

@@ -11,7 +11,8 @@ const connectionType = new GraphQLObjectType({
key: { type: GraphQLString },
data: { type: connectionDataType },
verified: { type: GraphQLBoolean },
app: { type: appType }
app: { type: appType },
createdAt: { type: GraphQLString }
}
}
})

View File

@@ -13,12 +13,14 @@
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/lodash.template": "^4.5.0",
"@types/luxon": "^2.0.8",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"clipboard-copy": "^4.0.1",
"graphql": "^15.6.0",
"lodash.template": "^4.5.0",
"luxon": "^2.2.0",
"notistack": "^2.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",

View File

@@ -2,9 +2,11 @@ import * as React from 'react';
import { useLazyQuery, useMutation } from '@apollo/client';
import Card from '@mui/material/Card';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import CardActionArea from '@mui/material/CardActionArea';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { useSnackbar } from 'notistack';
import { DateTime } from 'luxon';
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
@@ -17,7 +19,14 @@ type AppConnectionRowProps = {
connection: Connection;
}
const countTranslation = (value: React.ReactNode) => (<><strong>{value}</strong><br /></>);
const countTranslation = (value: React.ReactNode) => (
<>
<Typography variant="body1">
{value}
</Typography>
<br />
</>
);
function AppConnectionRow(props: AppConnectionRowProps) {
const { enqueueSnackbar } = useSnackbar();
@@ -25,7 +34,7 @@ function AppConnectionRow(props: AppConnectionRowProps) {
const [deleteConnection] = useMutation(DELETE_CONNECTION);
const formatMessage = useFormatMessage();
const { id, key, data, verified } = props.connection;
const { id, key, data, verified, createdAt } = props.connection;
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
@@ -57,23 +66,34 @@ function AppConnectionRow(props: AppConnectionRowProps) {
}
}, [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]);
const relativeCreatedAt = DateTime.fromMillis(parseInt(createdAt, 10)).toRelative();
return (
<>
<Card sx={{ my: 2 }}>
<CardActionArea onClick={onContextMenuClick}>
<CardContent>
<Box>
<Stack
direction="column"
justifyContent="center"
alignItems="flex-start"
spacing={1}
>
<Typography variant="h6">
{data.screenName}
</Typography>
</Box>
<Typography variant="caption">
{formatMessage('connection.addedAt', { datetime: relativeCreatedAt })}
</Typography>
</Stack>
<Box>
{testCalled && !testLoading && (verified ? 'yes' : 'no')}
</Box>
<Box sx={{ px: 2 }}>
<Typography variant="body2">
<Typography variant="caption" color="textSecondary" sx={{ display: ['none', 'inline-block'] }}>
{formatMessage('connection.flowCount', { count: countTranslation(0) })}
</Typography>
</Box>

View File

@@ -1,18 +1,34 @@
import * as React from 'react';
import { useQuery } from '@apollo/client';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import AppConnectionRow from 'components/AppConnectionRow';
import NoResultFound from 'components/NoResultFound';
import useFormatMessage from 'hooks/useFormatMessage';
import type { Connection } from 'types/connection';
import * as URLS from 'config/urls';
type AppConnectionsProps = {
appKey: String;
appKey: string;
}
export default function AppConnections(props: AppConnectionsProps) {
const { appKey } = props;
const formatMessage = useFormatMessage();
const { data } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey } });
const appConnections: Connection[] = data?.getApp?.connections || [];
const hasConnections = appConnections?.length;
if (!hasConnections) {
return (
<NoResultFound
to={URLS.APP_ADD_CONNECTION(appKey)}
text={formatMessage('app.noConnections')}
/>
);
}
return (
<>
{appConnections.map((appConnection: Connection) => (

View File

@@ -1,5 +1,6 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import type { AvatarProps } from '@mui/material/Avatar';
type AppIconProps = {
name: string;
@@ -11,18 +12,19 @@ const inlineImgStyle: React.CSSProperties = {
objectFit: 'contain',
};
export default function AppIcon(props: AppIconProps) {
const { name, url } = props;
export default function AppIcon(props: AppIconProps & AvatarProps) {
const { name, url, sx = {}, ...restProps } = props;
const color = url ? 'white' : props.color
return (
<Avatar
component="span"
variant="square"
sx={{ bgcolor: `#${color}` }}
sx={{ bgcolor: `#${color}`, display: 'inline-flex', width: 50, height: 50, ...sx }}
imgProps={{ style: inlineImgStyle }}
src={url}
alt={name}
{...restProps}
/>
);
};

View File

@@ -22,6 +22,12 @@ export default function Drawer(props: SwipeableDrawerProps) {
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
const formatMessage = useFormatMessage();
const closeOnClick = (event: React.SyntheticEvent) => {
if (matchSmallScreens) {
props.onClose(event);
}
}
return (
<BaseDrawer
{...props}
@@ -38,18 +44,21 @@ export default function Drawer(props: SwipeableDrawerProps) {
icon={<SwapCallsIcon htmlColor={theme.palette.primary.main} />}
primary={formatMessage('drawer.flows')}
to={URLS.FLOWS}
onClick={closeOnClick}
/>
<ListItemLink
icon={<AppsIcon htmlColor={theme.palette.primary.main} />}
primary={formatMessage('drawer.apps')}
to={URLS.APPS}
onClick={closeOnClick}
/>
<ListItemLink
icon={<ExploreIcon htmlColor={theme.palette.primary.main} />}
primary={formatMessage('drawer.explore')}
to={URLS.EXPLORE}
onClick={closeOnClick}
/>
</List>

View File

@@ -1,4 +1,4 @@
import { useMemo, forwardRef } from 'react';
import * as React from 'react';
import { useMatch } from 'react-router-dom';
import ListItem from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
@@ -9,15 +9,16 @@ type ListItemLinkProps = {
icon: React.ReactNode;
primary: string;
to: string;
onClick?: (event: React.SyntheticEvent) => void;
}
export default function ListItemLink(props: ListItemLinkProps) {
const { icon, primary, to } = props;
const { icon, primary, to, onClick } = props;
const selected = useMatch({ path: to, end: false });
const CustomLink = useMemo(
const CustomLink = React.useMemo(
() =>
forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InLineLink(
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InLineLink(
linkProps,
ref,
) {
@@ -28,7 +29,12 @@ export default function ListItemLink(props: ListItemLinkProps) {
return (
<li>
<ListItem component={CustomLink} sx={{ pl: { xs: 2, sm: 3 } }} selected={!!selected}>
<ListItem
component={CustomLink}
sx={{ pl: { xs: 2, sm: 3 } }}
selected={!!selected}
onClick={onClick}
>
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
<ListItemText primary={primary} primaryTypographyProps={{ variant: 'body1', }} />
</ListItem>

View File

@@ -0,0 +1,42 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import type { LinkProps } from 'react-router-dom';
import Card from '@mui/material/Card';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import CardActionArea from '@mui/material/CardActionArea';
import Typography from '@mui/material/Typography';
import { CardContent } from './style';
type NoResultFoundProps = {
text?: string;
to: string;
}
export default function NoResultFound(props: NoResultFoundProps) {
const { text, to } = props;
const ActionAreaLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
linkProps,
ref,
) {
return <Link ref={ref} to={to} {...linkProps} />;
}),
[to],
);
return (
<Card elevation={0}>
<CardActionArea component={ActionAreaLink} {...props}>
<CardContent>
<AddCircleIcon color="primary" />
<Typography variant="body1">
{text}
</Typography>
</CardContent>
</CardActionArea>
</Card>
)
};

View File

@@ -0,0 +1,11 @@
import { styled } from '@mui/material/styles';
import MuiCardContent from '@mui/material/CardContent';
export const CardContent = styled(MuiCardContent)`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
min-height: 200px;
`;

View File

@@ -11,6 +11,7 @@ export const GET_APP_CONNECTIONS = gql`
data {
screenName
}
createdAt
}
}
}

View File

@@ -9,9 +9,12 @@
"app.connectionCount": "{count} connections",
"app.flowCount": "{count} flows",
"app.addConnection": "Add connection",
"app.createFlow": "Create flow",
"app.settings": "Settings",
"app.connections": "Connections",
"app.noConnections": "You don't have any connections yet.",
"app.flows": "Flows",
"app.noFlows": "You don't have any flows yet.",
"apps.title": "My Apps",
"apps.addConnection": "Add connection",
"apps.addNewAppConnection": "Add a new app connection",
@@ -21,5 +24,7 @@
"connection.testConnection": "Test connection",
"connection.reconnect": "Reconnect",
"connection.delete": "Delete",
"connection.deletedMessage": "The connection has been deleted."
"connection.deletedMessage": "The connection has been deleted.",
"connection.addedAt": "Added {datetime}"
}

View File

@@ -1,17 +1,20 @@
import * as React from 'react';
import { useQuery } from '@apollo/client';
import { Link, Route, Navigate, Routes, useParams, useMatch, useNavigate } from 'react-router-dom';
import type { LinkProps } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import SettingsIcon from '@mui/icons-material/Settings';
import AddIcon from '@mui/icons-material/Add';
import useFormatMessage from 'hooks/useFormatMessage';
import { GET_APP } from 'graphql/queries/get-app';
import * as URLS from 'config/urls';
import ConditionalIconButton from 'components/ConditionalIconButton';
import AppConnections from 'components/AppConnections';
import AppFlows from 'components/AppFlows';
import AddAppConnection from 'components/AddAppConnection';
@@ -37,8 +40,9 @@ const ReconnectConnection = (props: any) => {
);
}
export default function Application() {
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
const formatMessage = useFormatMessage();
const connectionsPathMatch = useMatch({ path: URLS.APP_CONNECTIONS_PATTERN, end: false });
const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false });
@@ -49,34 +53,91 @@ export default function Application() {
const goToApplicationPage = () => navigate('connections');
const app = data?.getApp || {};
const NewConnectionLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
linkProps,
ref,
) {
return <Link ref={ref} to={URLS.APP_ADD_CONNECTION(appKey)} {...linkProps} />;
}),
[appKey],
);
const NewFlowLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
linkProps,
ref,
) {
return <Link ref={ref} to={URLS.APP_ADD_CONNECTION(appKey)} {...linkProps} />;
}),
[appKey],
);
return (
<>
<Box sx={{ py: 3 }}>
<Container>
<Grid container sx={{ mb: 3 }}>
<Grid item xs="auto" sx={{ mr: 1.5 }}>
<AppIcon url={app.iconUrl} color={app.primaryColor} name={app.name} />
<Grid container sx={{ mb: 3 }} alignItems="center">
<Grid item xs="auto" sx={{ mr: 3 }}>
<AppIcon
url={app.iconUrl}
color={app.primaryColor}
name={app.name}
/>
</Grid>
<Grid item xs>
<PageTitle>{app.name}</PageTitle>
</Grid>
<Grid item xs="auto" justifyContent="flex-end">
<IconButton sx={{ mr: 2 }} title={formatMessage('app.settings')}>
<SettingsIcon />
</IconButton>
<Grid item xs="auto">
<Routes>
<Route
path={`${URLS.FLOWS}/*`}
element={
<ConditionalIconButton
type="submit"
variant="contained"
color="primary"
size="large"
component={NewFlowLink}
fullWidth
icon={<AddIcon />}
>
{formatMessage('app.createFlow')}
</ConditionalIconButton>
}
/>
<Button variant="contained" component={Link} to={URLS.APP_ADD_CONNECTION(appKey)}>
<Route
path={`${URLS.CONNECTIONS}/*`}
element={
<ConditionalIconButton
type="submit"
variant="contained"
color="primary"
size="large"
component={NewConnectionLink}
fullWidth
icon={<AddIcon />}
>
{formatMessage('app.addConnection')}
</Button>
</ConditionalIconButton>
}
/>
</Routes>
</Grid>
</Grid>
<Grid container>
<Grid item xs>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
<Tabs value={connectionsPathMatch?.pattern?.path || flowsPathMatch?.pattern?.path}>
<Tabs
variant={matchSmallScreens ? 'fullWidth' : undefined}
value={connectionsPathMatch?.pattern?.path || flowsPathMatch?.pattern?.path}
>
<Tab
label={formatMessage('app.connections')}
to={URLS.APP_CONNECTIONS(appKey)}

View File

@@ -6,7 +6,6 @@ import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import AddIcon from '@mui/icons-material/Add';
import ConditionalIconButton from 'components/ConditionalIconButton';
import Container from 'components/Container';
import AddNewAppConnection from 'components/AddNewAppConnection';

View File

@@ -1,5 +1,5 @@
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
import Container from 'components/Container';
export default function Explore() {
return (

View File

@@ -180,6 +180,9 @@ const extendedTheme = createTheme({
styleOverrides: {
root: {
background: 'rgba(0, 8, 20, 0.64)'
},
invisible: {
background: 'transparent',
}
}
},
@@ -243,6 +246,15 @@ const extendedTheme = createTheme({
}
}
},
MuiTab: {
styleOverrides: {
root: {
[referenceTheme.breakpoints.up('sm')]: {
padding: referenceTheme.spacing(1.5, 3),
}
}
},
},
MuiToolbar: {
styleOverrides: {
root: {

View File

@@ -7,6 +7,7 @@ type Connection = {
key: string;
data: ConnectionData;
verified: boolean;
createdAt: string;
};
export type { Connection, ConnectionData };

View File

@@ -3206,6 +3206,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==
"@types/luxon@^2.0.8":
version "2.0.8"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.0.8.tgz#a0fdd7ab0b67e08bf1d301232a7fef79b74ded69"
integrity sha512-lGmxL6hMEVqXr8w9bL52RUWXVu90o7vH8WQSutQssr2e+w0TNttXx2Zfw2V2lHHHWfW6OGqB8bXDvtKocv19qQ==
"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
@@ -10531,6 +10536,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
luxon@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.2.0.tgz#f5c4a234ba4016f792488b11aaed2d5bc14c888e"
integrity sha512-LwmknessH4jVIseCsizUgveIHwlLv/RQZWC2uDSMfGJs7w8faPUi2JFxfyfMcTPrpNbChTem3Uz6IKRtn+LcIA==
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"