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')}
+
+ }
+ />
+
-
+