From 2293c939e728af71edef7c60980535002eacfea6 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 18 Oct 2021 23:58:40 +0200 Subject: [PATCH] feat: add app connections w/ testing and deleting functions --- packages/backend/src/apps/twitter/index.ts | 3 +- .../graphql/mutations/create-connection.ts | 7 +- .../graphql/queries/get-app-connections.ts | 9 +- .../backend/src/graphql/queries/get-app.ts | 23 ++++- .../src/graphql/queries/test-connection.ts | 2 +- packages/backend/src/graphql/types/app.ts | 23 +++-- .../backend/src/graphql/types/connection.ts | 15 ++- .../components/AddNewAppConnection/index.tsx | 2 +- .../AppConnectionContextMenu/index.tsx | 57 +++++++++++ .../src/components/AppConnectionRow/index.tsx | 94 +++++++++++++++++++ .../src/components/AppConnectionRow/style.ts | 17 ++++ .../src/components/AppConnections/index.tsx | 23 +++++ packages/web/src/components/AppRow/index.tsx | 2 +- .../web/src/components/InputCreator/index.tsx | 2 - packages/web/src/components/Layout/index.tsx | 2 +- packages/web/src/graphql/cache.ts | 25 +++++ .../graphql/mutations/create-connection.ts | 9 +- .../graphql/mutations/delete-connection.ts | 7 ++ packages/web/src/graphql/mutations/index.ts | 4 +- .../graphql/mutations/update-connection.ts | 3 + .../graphql/queries/get-app-connections.ts | 17 ++++ packages/web/src/graphql/queries/get-app.ts | 3 + packages/web/src/graphql/queries/get-apps.ts | 3 + .../src/graphql/queries/test-connection.ts | 7 ++ packages/web/src/locales/en.json | 7 +- packages/web/src/pages/Application/index.tsx | 5 +- packages/web/src/types/connection.ts | 12 +++ 27 files changed, 349 insertions(+), 34 deletions(-) create mode 100644 packages/web/src/components/AppConnectionContextMenu/index.tsx create mode 100644 packages/web/src/components/AppConnectionRow/index.tsx create mode 100644 packages/web/src/components/AppConnectionRow/style.ts create mode 100644 packages/web/src/components/AppConnections/index.tsx create mode 100644 packages/web/src/graphql/mutations/delete-connection.ts create mode 100644 packages/web/src/graphql/queries/get-app-connections.ts create mode 100644 packages/web/src/graphql/queries/test-connection.ts create mode 100644 packages/web/src/types/connection.ts 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 ( + + + {formatMessage('connection.viewFlows')} + + + + {formatMessage('connection.testConnection')} + + + + {formatMessage('connection.reconnect')} + + + + {formatMessage('connection.delete')} + + + ); +}; 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 };