diff --git a/packages/backend/src/graphql/types/connection.ts b/packages/backend/src/graphql/types/connection.ts index b677e8c2..5ed068f1 100644 --- a/packages/backend/src/graphql/types/connection.ts +++ b/packages/backend/src/graphql/types/connection.ts @@ -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 } } } }) diff --git a/packages/web/package.json b/packages/web/package.json index 0e60102d..d6094fec 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -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", diff --git a/packages/web/src/components/AppConnectionRow/index.tsx b/packages/web/src/components/AppConnectionRow/index.tsx index dd8773ef..45e6cdd1 100644 --- a/packages/web/src/components/AppConnectionRow/index.tsx +++ b/packages/web/src/components/AppConnectionRow/index.tsx @@ -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) => (<>{value}
); +const countTranslation = (value: React.ReactNode) => ( + <> + + {value} + +
+ +); 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(null); const [anchorEl, setAnchorEl] = React.useState(null); @@ -57,23 +66,34 @@ function AppConnectionRow(props: AppConnectionRowProps) { } }, [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]); + const relativeCreatedAt = DateTime.fromMillis(parseInt(createdAt, 10)).toRelative(); + return ( <> - + {data.screenName} - + + + {formatMessage('connection.addedAt', { datetime: relativeCreatedAt })} + + {testCalled && !testLoading && (verified ? 'yes' : 'no')} - + {formatMessage('connection.flowCount', { count: countTranslation(0) })} diff --git a/packages/web/src/components/AppConnections/index.tsx b/packages/web/src/components/AppConnections/index.tsx index 5e32b759..7a01de02 100644 --- a/packages/web/src/components/AppConnections/index.tsx +++ b/packages/web/src/components/AppConnections/index.tsx @@ -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 ( + + ); + } + return ( <> {appConnections.map((appConnection: Connection) => ( diff --git a/packages/web/src/components/AppIcon/index.tsx b/packages/web/src/components/AppIcon/index.tsx index adfdb30c..a9ba9137 100644 --- a/packages/web/src/components/AppIcon/index.tsx +++ b/packages/web/src/components/AppIcon/index.tsx @@ -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 ( ); }; diff --git a/packages/web/src/components/Drawer/index.tsx b/packages/web/src/components/Drawer/index.tsx index 3105a5d4..0456d49c 100644 --- a/packages/web/src/components/Drawer/index.tsx +++ b/packages/web/src/components/Drawer/index.tsx @@ -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 ( } primary={formatMessage('drawer.flows')} to={URLS.FLOWS} + onClick={closeOnClick} /> } primary={formatMessage('drawer.apps')} to={URLS.APPS} + onClick={closeOnClick} /> } primary={formatMessage('drawer.explore')} to={URLS.EXPLORE} + onClick={closeOnClick} /> diff --git a/packages/web/src/components/ListItemLink/index.tsx b/packages/web/src/components/ListItemLink/index.tsx index b2010b74..5f910155 100644 --- a/packages/web/src/components/ListItemLink/index.tsx +++ b/packages/web/src/components/ListItemLink/index.tsx @@ -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>(function InLineLink( + React.forwardRef>(function InLineLink( linkProps, ref, ) { @@ -28,7 +29,12 @@ export default function ListItemLink(props: ListItemLinkProps) { return (
  • - + {icon} diff --git a/packages/web/src/components/NoResultFound/index.tsx b/packages/web/src/components/NoResultFound/index.tsx new file mode 100644 index 00000000..4227d48a --- /dev/null +++ b/packages/web/src/components/NoResultFound/index.tsx @@ -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>(function InlineLink( + linkProps, + ref, + ) { + return ; + }), + [to], + ); + + return ( + + + + + + + {text} + + + + + ) +}; diff --git a/packages/web/src/components/NoResultFound/style.ts b/packages/web/src/components/NoResultFound/style.ts new file mode 100644 index 00000000..eedbaff9 --- /dev/null +++ b/packages/web/src/components/NoResultFound/style.ts @@ -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; +`; \ No newline at end of file diff --git a/packages/web/src/graphql/queries/get-app-connections.ts b/packages/web/src/graphql/queries/get-app-connections.ts index 22b9a50a..81c64060 100644 --- a/packages/web/src/graphql/queries/get-app-connections.ts +++ b/packages/web/src/graphql/queries/get-app-connections.ts @@ -11,6 +11,7 @@ export const GET_APP_CONNECTIONS = gql` data { screenName } + createdAt } } } diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index f18a2d04..7001e536 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -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}" + } \ No newline at end of file diff --git a/packages/web/src/pages/Application/index.tsx b/packages/web/src/pages/Application/index.tsx index c2e8b231..01da5ed2 100644 --- a/packages/web/src/pages/Application/index.tsx +++ b/packages/web/src/pages/Application/index.tsx @@ -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>(function InlineLink( + linkProps, + ref, + ) { + return ; + }), + [appKey], + ); + + const NewFlowLink = React.useMemo( + () => + React.forwardRef>(function InlineLink( + linkProps, + ref, + ) { + return ; + }), + [appKey], + ); + return ( <> - - - + + + {app.name} - - - - + + + } + > + {formatMessage('app.createFlow')} + + } + /> - + } + > + {formatMessage('app.addConnection')} + + } + /> + - +