diff --git a/packages/backend/src/apps/twitter/index.ts b/packages/backend/src/apps/twitter/index.ts
index 8771bd36..31272c72 100644
--- a/packages/backend/src/apps/twitter/index.ts
+++ b/packages/backend/src/apps/twitter/index.ts
@@ -1,6 +1,7 @@
import TwitterApi from 'twitter-api-v2';
import App from '../../models/app';
import Field from '../../types/field';
+import appData from './info';
export default class Twitter {
client: any
@@ -16,7 +17,7 @@ export default class Twitter {
});
this.connectionData = connectionData;
- this.appData = App.findOneByKey('twitter');
+ this.appData = appData;
}
async createAuthLink() {
diff --git a/packages/backend/src/graphql/mutations/create-connection.ts b/packages/backend/src/graphql/mutations/create-connection.ts
index 3f14b042..e659de80 100644
--- a/packages/backend/src/graphql/mutations/create-connection.ts
+++ b/packages/backend/src/graphql/mutations/create-connection.ts
@@ -1,5 +1,6 @@
import { GraphQLString, GraphQLNonNull } from 'graphql';
import Connection from '../../models/connection';
+import App from '../../models/app';
import connectionType from '../types/connection';
import twitterCredentialInputType from '../types/twitter-credential-input';
import RequestWithCurrentUser from '../../types/express/request-with-current-user';
@@ -9,13 +10,17 @@ type Params = {
data: object
}
const createConnectionResolver = async (params: Params, req: RequestWithCurrentUser) => {
+ const app = await App.findOneByKey(params.key);
const connection = await Connection.query().insert({
key: params.key,
data: params.data,
userId: req.currentUser.id
});
- return connection;
+ return {
+ ...connection,
+ app,
+ };
}
const createConnection = {
diff --git a/packages/backend/src/graphql/queries/get-app-connections.ts b/packages/backend/src/graphql/queries/get-app-connections.ts
index 129b5b54..08d8bbc5 100644
--- a/packages/backend/src/graphql/queries/get-app-connections.ts
+++ b/packages/backend/src/graphql/queries/get-app-connections.ts
@@ -1,5 +1,6 @@
import { GraphQLList, GraphQLString, GraphQLNonNull } from 'graphql';
import Connection from '../../models/connection';
+import App from '../../models/app';
import RequestWithCurrentUser from '../../types/express/request-with-current-user';
import connectionType from '../types/connection';
@@ -8,10 +9,14 @@ type Params = {
}
const getAppConnectionsResolver = async (params: Params, req: RequestWithCurrentUser) => {
+ const app = await App.findOneByKey(params.key);
const connections = await Connection.query()
- .where({ user_id: req.currentUser.id, verified: true, key: params.key })
+ .where({ user_id: req.currentUser.id, key: params.key })
- return connections;
+ return connections.map((connection: any) => ({
+ ...connection,
+ app,
+ }));
}
const getAppConnections = {
diff --git a/packages/backend/src/graphql/queries/get-app.ts b/packages/backend/src/graphql/queries/get-app.ts
index 8e05ea3e..44701c1b 100644
--- a/packages/backend/src/graphql/queries/get-app.ts
+++ b/packages/backend/src/graphql/queries/get-app.ts
@@ -1,17 +1,32 @@
import { GraphQLString, GraphQLNonNull } from 'graphql';
import App from '../../models/app';
import appType from '../types/app';
+import RequestWithCurrentUser from '../../types/express/request-with-current-user';
+import Connection from '../../models/connection';
+import connectionType from '../types/connection';
type Params = {
key: string
}
-const getAppResolver = (params: Params) => {
+const getAppResolver = async (params: Params, req: RequestWithCurrentUser) => {
if(!params.key) {
throw new Error('No key provided.')
}
- return App.findOneByKey(params.key)
+ const app = await App.findOneByKey(params.key);
+
+ if (req.currentUser?.id) {
+ const connections = await Connection.query()
+ .where({ user_id: req.currentUser.id, key: params.key });
+
+ return {
+ ...app,
+ connections,
+ };
+ }
+
+ return app;
}
const getApp = {
@@ -19,9 +34,7 @@ const getApp = {
args: {
key: { type: GraphQLNonNull(GraphQLString) },
},
- resolve: (_: any, params: Params) => getAppResolver(params)
+ resolve: (_: any, params: Params, req: RequestWithCurrentUser) => getAppResolver(params, req)
}
-
-
export default getApp;
diff --git a/packages/backend/src/graphql/queries/test-connection.ts b/packages/backend/src/graphql/queries/test-connection.ts
index 9c90ba15..e545d7e9 100644
--- a/packages/backend/src/graphql/queries/test-connection.ts
+++ b/packages/backend/src/graphql/queries/test-connection.ts
@@ -15,7 +15,7 @@ const testConnectionResolver = async (params: Params, req: RequestWithCurrentUse
const appClass = (await import(`../../apps/${connection.key}`)).default;
- const appInstance = new appClass(connection.data)
+ const appInstance = new appClass(connection.data);
const isStillVerified = await appInstance.isStillVerified();
connection = await connection.$query().patchAndFetch({
diff --git a/packages/backend/src/graphql/types/app.ts b/packages/backend/src/graphql/types/app.ts
index 8ce780cc..535eb048 100644
--- a/packages/backend/src/graphql/types/app.ts
+++ b/packages/backend/src/graphql/types/app.ts
@@ -4,15 +4,20 @@ import authenticationStepType from './authentication-step';
const appType = new GraphQLObjectType({
name: 'App',
- fields: {
- name: { type: GraphQLString },
- key: { type: GraphQLString },
- connectionCount: { type: GraphQLInt },
- iconUrl: { type: GraphQLString },
- docUrl: { type: GraphQLString },
- primaryColor: { type: GraphQLString },
- fields: { type: GraphQLList(fieldType) },
- authenticationSteps: { type: GraphQLList(authenticationStepType) }
+ fields: () => {
+ const connectionType = require('./connection').default;
+
+ return {
+ name: { type: GraphQLString },
+ key: { type: GraphQLString },
+ connectionCount: { type: GraphQLInt },
+ iconUrl: { type: GraphQLString },
+ docUrl: { type: GraphQLString },
+ primaryColor: { type: GraphQLString },
+ fields: { type: GraphQLList(fieldType) },
+ authenticationSteps: { type: GraphQLList(authenticationStepType) },
+ connections: { type: GraphQLList(connectionType) },
+ }
}
});
diff --git a/packages/backend/src/graphql/types/connection.ts b/packages/backend/src/graphql/types/connection.ts
index 49fd4df2..415994db 100644
--- a/packages/backend/src/graphql/types/connection.ts
+++ b/packages/backend/src/graphql/types/connection.ts
@@ -3,11 +3,16 @@ import connectionDataType from './connection-data';
const connectionType = new GraphQLObjectType({
name: 'connection',
- fields: {
- id: { type: GraphQLString },
- key: { type: GraphQLString },
- data: { type: connectionDataType },
- verified: { type: GraphQLBoolean },
+ fields: () => {
+ const appType = require('./app').default;
+
+ return {
+ id: { type: GraphQLString },
+ key: { type: GraphQLString },
+ data: { type: connectionDataType },
+ verified: { type: GraphQLBoolean },
+ app: { type: appType }
+ }
}
})
diff --git a/packages/web/src/components/AddNewAppConnection/index.tsx b/packages/web/src/components/AddNewAppConnection/index.tsx
index 40b20c6f..883e0993 100644
--- a/packages/web/src/components/AddNewAppConnection/index.tsx
+++ b/packages/web/src/components/AddNewAppConnection/index.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useState } from 'react';
+import { useState } from 'react';
import { useQuery } from '@apollo/client';
import { Link } from 'react-router-dom';
import DialogTitle from '@mui/material/DialogTitle';
diff --git a/packages/web/src/components/AppConnectionContextMenu/index.tsx b/packages/web/src/components/AppConnectionContextMenu/index.tsx
new file mode 100644
index 00000000..c51ec851
--- /dev/null
+++ b/packages/web/src/components/AppConnectionContextMenu/index.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react';
+import { Link } from 'react-router-dom';
+import Menu from '@mui/material/Menu';
+import type { PopoverProps } from '@mui/material/Popover';
+import MenuItem from '@mui/material/MenuItem';
+
+import * as URLS from 'config/urls';
+import useFormatMessage from 'hooks/useFormatMessage';
+
+type Action = {
+ type: 'test' | 'reconnect' | 'delete' | 'viewFlows';
+};
+
+type ContextMenuProps = {
+ appKey: string;
+ onClose: () => void;
+ onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
+ anchorEl: PopoverProps['anchorEl'];
+};
+
+export default function ContextMenu(props: ContextMenuProps) {
+ const { appKey, onClose, onMenuItemClick, anchorEl } = props;
+ const formatMessage = useFormatMessage();
+
+ const createActionHandler = React.useCallback((action: Action) => {
+ return function clickHandler(event: React.MouseEvent) {
+ onMenuItemClick(event, action);
+
+ onClose();
+ };
+ }, [onMenuItemClick, onClose]);
+
+ return (
+
+ );
+};
diff --git a/packages/web/src/components/AppConnectionRow/index.tsx b/packages/web/src/components/AppConnectionRow/index.tsx
new file mode 100644
index 00000000..7734c157
--- /dev/null
+++ b/packages/web/src/components/AppConnectionRow/index.tsx
@@ -0,0 +1,94 @@
+import * as React from 'react';
+import { useLazyQuery, useMutation } from '@apollo/client';
+import Card from '@mui/material/Card';
+import Box from '@mui/material/Box';
+import CardActionArea from '@mui/material/CardActionArea';
+import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
+
+import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
+import { TEST_CONNECTION } from 'graphql/queries/test-connection';
+import ConnectionContextMenu from 'components/AppConnectionContextMenu';
+import useFormatMessage from 'hooks/useFormatMessage';
+import type { Connection } from 'types/connection';
+import { CardContent, Typography } from './style';
+
+type AppConnectionRowProps = {
+ connection: Connection;
+}
+
+const countTranslation = (value: React.ReactNode) => (<>{value}
>);
+
+function AppConnectionRow(props: AppConnectionRowProps) {
+ const [testConnection, { data: testData, called: testCalled, loading: testLoading }] = useLazyQuery(TEST_CONNECTION);
+ const [deleteConnection] = useMutation(DELETE_CONNECTION);
+
+ const formatMessage = useFormatMessage();
+ const { id, key, data } = props.connection;
+
+ const contextButtonRef = React.useRef(null);
+ const [anchorEl, setAnchorEl] = React.useState(null);
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const onContextMenuClick = (event: React.MouseEvent) => setAnchorEl(contextButtonRef.current);
+ const onContextMenuAction = React.useCallback((event, action: { [key: string]: string }) => {
+ if (action.type === 'delete') {
+ deleteConnection({
+ variables: { id },
+ update: (cache, mutationResult) => {
+ const connectionCacheId = cache.identify({
+ __typename: 'connection',
+ id,
+ });
+
+ cache.evict({
+ id: connectionCacheId,
+ });
+ }
+ });
+ } else if (action.type === 'test') {
+ testConnection({ variables: { id } });
+ }
+ }, [deleteConnection, id, testConnection]);
+
+ return (
+ <>
+
+
+
+
+
+ {data.screenName}
+
+
+
+
+ {testCalled && !testLoading && (testData ? 'yes' : 'no')}
+
+
+
+
+ {formatMessage('connection.flowCount', { count: countTranslation(0) })}
+
+
+
+
+
+
+
+
+
+
+ {anchorEl && }
+ >
+ );
+}
+
+export default AppConnectionRow;
diff --git a/packages/web/src/components/AppConnectionRow/style.ts b/packages/web/src/components/AppConnectionRow/style.ts
new file mode 100644
index 00000000..30f9967b
--- /dev/null
+++ b/packages/web/src/components/AppConnectionRow/style.ts
@@ -0,0 +1,17 @@
+import { styled } from '@mui/material/styles';
+import MuiCardContent from '@mui/material/CardContent';
+import MuiTypography from '@mui/material/Typography';
+
+export const CardContent = styled(MuiCardContent)(({ theme }) => ({
+ display: 'grid',
+ gridTemplateRows: 'auto',
+ gridTemplateColumns: '1fr auto auto auto',
+ gridColumnGap: theme.spacing(2),
+ alignItems: 'center',
+}));
+
+
+export const Typography = styled(MuiTypography)(({ theme }) => ({
+ textAlign: 'center',
+ display: 'inline-block',
+}));
diff --git a/packages/web/src/components/AppConnections/index.tsx b/packages/web/src/components/AppConnections/index.tsx
new file mode 100644
index 00000000..02095a98
--- /dev/null
+++ b/packages/web/src/components/AppConnections/index.tsx
@@ -0,0 +1,23 @@
+import { useQuery } from '@apollo/client';
+import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
+
+import AppConnectionRow from 'components/AppConnectionRow';
+import type { Connection } from 'types/connection';
+
+type AppConnectionsProps = {
+ appKey: String;
+}
+
+export default function AppConnections(props: AppConnectionsProps) {
+ const { appKey: key } = props;
+ const { data } = useQuery(GET_APP_CONNECTIONS, { variables: { key } });
+ const appConnections: Connection[] = data?.getApp?.connections || [];
+
+ return (
+ <>
+ {appConnections.map((appConnection: Connection) => (
+
+ ))}
+ >
+ )
+};
diff --git a/packages/web/src/components/AppRow/index.tsx b/packages/web/src/components/AppRow/index.tsx
index 58675c46..36206e26 100644
--- a/packages/web/src/components/AppRow/index.tsx
+++ b/packages/web/src/components/AppRow/index.tsx
@@ -24,7 +24,7 @@ function AppRow(props: AppRowProps) {
-
+
diff --git a/packages/web/src/components/InputCreator/index.tsx b/packages/web/src/components/InputCreator/index.tsx
index 330aa958..8db984a2 100644
--- a/packages/web/src/components/InputCreator/index.tsx
+++ b/packages/web/src/components/InputCreator/index.tsx
@@ -19,12 +19,10 @@ export default function InputCreator(props: InputCreatorProps) {
const {
key: name,
label,
- type,
required,
readOnly,
value,
description,
- docUrl,
clickToCopy,
} = schema;
diff --git a/packages/web/src/components/Layout/index.tsx b/packages/web/src/components/Layout/index.tsx
index b6ac072e..944c6401 100644
--- a/packages/web/src/components/Layout/index.tsx
+++ b/packages/web/src/components/Layout/index.tsx
@@ -9,7 +9,7 @@ type LayoutProps = {
}
export default function Layout({ children }: LayoutProps) {
- const [isDrawerOpen, setDrawerOpen] = useState(false);
+ const [isDrawerOpen, setDrawerOpen] = useState(true);
const onMenuClick = useCallback(() => { setDrawerOpen(value => !value) }, []);
return (
diff --git a/packages/web/src/graphql/cache.ts b/packages/web/src/graphql/cache.ts
index f834f1fa..4a7dda37 100644
--- a/packages/web/src/graphql/cache.ts
+++ b/packages/web/src/graphql/cache.ts
@@ -4,6 +4,31 @@ const cache = new InMemoryCache({
typePolicies: {
App: {
keyFields: ['key']
+ },
+ Mutation: {
+ mutationType: true,
+ fields: {
+ createConnection: {
+ merge(existing, newConnection, { args, readField, cache }) {
+ const appKey = readField('key', newConnection);
+ const appCacheId = cache.identify({
+ __typename: 'App',
+ key: appKey,
+ });
+
+ cache.modify({
+ id: appCacheId,
+ fields: {
+ connections: (existingConnections) => {
+ return [...existingConnections, newConnection];
+ }
+ }
+ });
+
+ return newConnection;
+ }
+ }
+ }
}
}
});
diff --git a/packages/web/src/graphql/mutations/create-connection.ts b/packages/web/src/graphql/mutations/create-connection.ts
index c505b91f..1380a38a 100644
--- a/packages/web/src/graphql/mutations/create-connection.ts
+++ b/packages/web/src/graphql/mutations/create-connection.ts
@@ -3,8 +3,15 @@ import { gql } from '@apollo/client';
export const CREATE_CONNECTION = gql`
mutation CreateConnection($key: String!, $data: twitterCredentialInput!) {
createConnection(key: $key, data: $data) {
- key
id
+ key
+ verified
+ data {
+ screenName
+ }
+ app {
+ key
+ }
}
}
`;
diff --git a/packages/web/src/graphql/mutations/delete-connection.ts b/packages/web/src/graphql/mutations/delete-connection.ts
new file mode 100644
index 00000000..8020a5a6
--- /dev/null
+++ b/packages/web/src/graphql/mutations/delete-connection.ts
@@ -0,0 +1,7 @@
+import { gql } from '@apollo/client';
+
+export const DELETE_CONNECTION = gql`
+ mutation DeleteConnection($id: String!) {
+ deleteConnection(id: $id)
+ }
+`;
diff --git a/packages/web/src/graphql/mutations/index.ts b/packages/web/src/graphql/mutations/index.ts
index 8e46f88d..d2d3a47b 100644
--- a/packages/web/src/graphql/mutations/index.ts
+++ b/packages/web/src/graphql/mutations/index.ts
@@ -1,6 +1,7 @@
import { CREATE_CONNECTION } from './create-connection';
-import { CREATE_AUTH_LINK } from './create-auth-link';
import { UPDATE_CONNECTION } from './update-connection';
+import { DELETE_CONNECTION } from './delete-connection';
+import { CREATE_AUTH_LINK } from './create-auth-link';
type Mutations = {
[key: string]: any,
@@ -9,6 +10,7 @@ type Mutations = {
const mutations: Mutations = {
createConnection: CREATE_CONNECTION,
updateConnection: UPDATE_CONNECTION,
+ deleteConnection: DELETE_CONNECTION,
createAuthLink: CREATE_AUTH_LINK,
};
diff --git a/packages/web/src/graphql/mutations/update-connection.ts b/packages/web/src/graphql/mutations/update-connection.ts
index 6e4ead32..c637d81a 100644
--- a/packages/web/src/graphql/mutations/update-connection.ts
+++ b/packages/web/src/graphql/mutations/update-connection.ts
@@ -6,6 +6,9 @@ export const UPDATE_CONNECTION = gql`
id
key
verified
+ data {
+ screenName
+ }
}
}
`;
diff --git a/packages/web/src/graphql/queries/get-app-connections.ts b/packages/web/src/graphql/queries/get-app-connections.ts
new file mode 100644
index 00000000..ae0ffc89
--- /dev/null
+++ b/packages/web/src/graphql/queries/get-app-connections.ts
@@ -0,0 +1,17 @@
+import { gql } from '@apollo/client';
+
+export const GET_APP_CONNECTIONS = gql`
+ query GetAppConnections($key: String!) {
+ getApp(key: $key) {
+ key
+ connections {
+ id
+ key
+ verified
+ data {
+ screenName
+ }
+ }
+ }
+ }
+`;
diff --git a/packages/web/src/graphql/queries/get-app.ts b/packages/web/src/graphql/queries/get-app.ts
index 18dfd91e..67ed318a 100644
--- a/packages/web/src/graphql/queries/get-app.ts
+++ b/packages/web/src/graphql/queries/get-app.ts
@@ -32,6 +32,9 @@ export const GET_APP = gql`
}
}
}
+ connections {
+ id
+ }
}
}
`;
diff --git a/packages/web/src/graphql/queries/get-apps.ts b/packages/web/src/graphql/queries/get-apps.ts
index 54106964..36a0a536 100644
--- a/packages/web/src/graphql/queries/get-apps.ts
+++ b/packages/web/src/graphql/queries/get-apps.ts
@@ -19,6 +19,9 @@ export const GET_APPS = gql`
docUrl
clickToCopy
}
+ connections {
+ id
+ }
}
}
`;
\ No newline at end of file
diff --git a/packages/web/src/graphql/queries/test-connection.ts b/packages/web/src/graphql/queries/test-connection.ts
new file mode 100644
index 00000000..5a0275de
--- /dev/null
+++ b/packages/web/src/graphql/queries/test-connection.ts
@@ -0,0 +1,7 @@
+import { gql } from '@apollo/client';
+
+export const TEST_CONNECTION = gql`
+ query TestConnection($id: String!) {
+ testConnection(id: $id)
+ }
+`;
\ No newline at end of file
diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json
index 03b6bb96..d5f9fd1f 100644
--- a/packages/web/src/locales/en.json
+++ b/packages/web/src/locales/en.json
@@ -15,5 +15,10 @@
"apps.title": "Apps",
"apps.addConnection": "Add connection",
"apps.addNewAppConnection": "Add a new app connection",
- "apps.searchApp": "Search for app"
+ "apps.searchApp": "Search for app",
+ "connection.flowCount": "{count} flows",
+ "connection.viewFlows": "View flows",
+ "connection.testConnection": "Test connection",
+ "connection.reconnect": "Reconnect",
+ "connection.delete": "Delete"
}
\ 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 c77aed7f..310a3fd2 100644
--- a/packages/web/src/pages/Application/index.tsx
+++ b/packages/web/src/pages/Application/index.tsx
@@ -12,6 +12,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
import { GET_APP } from 'graphql/queries/get-app';
import * as URLS from 'config/urls';
+import AppConnections from 'components/AppConnections';
import AddAppConnection from 'components/AddAppConnection';
import AppIcon from 'components/AppIcon';
import Container from 'components/Container';
@@ -78,11 +79,11 @@ export default function Application() {
- Flows come here.
+ Flows
- Connections come here.
+
diff --git a/packages/web/src/types/connection.ts b/packages/web/src/types/connection.ts
new file mode 100644
index 00000000..303c7816
--- /dev/null
+++ b/packages/web/src/types/connection.ts
@@ -0,0 +1,12 @@
+type ConnectionData = {
+ screenName: string;
+}
+
+type Connection = {
+ id: string;
+ key: string;
+ data: ConnectionData;
+ verified: boolean;
+};
+
+export type { Connection, ConnectionData };