feat: add app connections w/ testing and deleting functions

This commit is contained in:
Ali BARIN
2021-10-18 23:58:40 +02:00
parent 672cc4c60c
commit 2293c939e7
27 changed files with 349 additions and 34 deletions

View File

@@ -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() {

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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({

View File

@@ -4,7 +4,10 @@ import authenticationStepType from './authentication-step';
const appType = new GraphQLObjectType({
name: 'App',
fields: {
fields: () => {
const connectionType = require('./connection').default;
return {
name: { type: GraphQLString },
key: { type: GraphQLString },
connectionCount: { type: GraphQLInt },
@@ -12,7 +15,9 @@ const appType = new GraphQLObjectType({
docUrl: { type: GraphQLString },
primaryColor: { type: GraphQLString },
fields: { type: GraphQLList(fieldType) },
authenticationSteps: { type: GraphQLList(authenticationStepType) }
authenticationSteps: { type: GraphQLList(authenticationStepType) },
connections: { type: GraphQLList(connectionType) },
}
}
});

View File

@@ -3,11 +3,16 @@ import connectionDataType from './connection-data';
const connectionType = new GraphQLObjectType({
name: 'connection',
fields: {
fields: () => {
const appType = require('./app').default;
return {
id: { type: GraphQLString },
key: { type: GraphQLString },
data: { type: connectionDataType },
verified: { type: GraphQLBoolean },
app: { type: appType }
}
}
})

View File

@@ -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';

View File

@@ -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 (
<Menu
open={true}
onClose={onClose}
hideBackdrop={false}
anchorEl={anchorEl}
>
<MenuItem component={Link} to={URLS.APP_FLOWS(appKey)} onClick={createActionHandler({ type: 'viewFlows' })}>
{formatMessage('connection.viewFlows')}
</MenuItem>
<MenuItem onClick={createActionHandler({ type: 'test' })}>
{formatMessage('connection.testConnection')}
</MenuItem>
<MenuItem onClick={createActionHandler({ type: 'reconnect' })}>
{formatMessage('connection.reconnect')}
</MenuItem>
<MenuItem onClick={createActionHandler({ type: 'delete' })}>
{formatMessage('connection.delete')}
</MenuItem>
</Menu>
);
};

View File

@@ -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) => (<><strong>{value}</strong><br /></>);
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<SVGSVGElement | null>(null);
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
const handleClose = () => {
setAnchorEl(null);
};
const onContextMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => 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 (
<>
<Card sx={{ my: 2 }}>
<CardActionArea onClick={onContextMenuClick}>
<CardContent>
<Box>
<Typography variant="h6">
{data.screenName}
</Typography>
</Box>
<Box>
{testCalled && !testLoading && (testData ? 'yes' : 'no')}
</Box>
<Box sx={{ px: 2 }}>
<Typography variant="body2">
{formatMessage('connection.flowCount', { count: countTranslation(0) })}
</Typography>
</Box>
<Box>
<MoreHorizIcon ref={contextButtonRef} />
</Box>
</CardContent>
</CardActionArea>
</Card>
{anchorEl && <ConnectionContextMenu
appKey={key}
onClose={handleClose}
onMenuItemClick={onContextMenuAction}
anchorEl={anchorEl}
/>}
</>
);
}
export default AppConnectionRow;

View File

@@ -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',
}));

View File

@@ -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) => (
<AppConnectionRow key={appConnection.id} connection={appConnection} />
))}
</>
)
};

View File

@@ -19,12 +19,10 @@ export default function InputCreator(props: InputCreatorProps) {
const {
key: name,
label,
type,
required,
readOnly,
value,
description,
docUrl,
clickToCopy,
} = schema;

View File

@@ -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 (

View File

@@ -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;
}
}
}
}
}
});

View File

@@ -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
}
}
}
`;

View File

@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_CONNECTION = gql`
mutation DeleteConnection($id: String!) {
deleteConnection(id: $id)
}
`;

View File

@@ -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,
};

View File

@@ -6,6 +6,9 @@ export const UPDATE_CONNECTION = gql`
id
key
verified
data {
screenName
}
}
}
`;

View File

@@ -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
}
}
}
}
`;

View File

@@ -32,6 +32,9 @@ export const GET_APP = gql`
}
}
}
connections {
id
}
}
}
`;

View File

@@ -19,6 +19,9 @@ export const GET_APPS = gql`
docUrl
clickToCopy
}
connections {
id
}
}
}
`;

View File

@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const TEST_CONNECTION = gql`
query TestConnection($id: String!) {
testConnection(id: $id)
}
`;

View File

@@ -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"
}

View File

@@ -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() {
<Switch>
<Route path={URLS.APP_FLOWS_PATTERN}>
Flows come here.
Flows
</Route>
<Route path={URLS.APP_CONNECTIONS_PATTERN}>
Connections come here.
<AppConnections appKey={key} />
</Route>
<Route exact path={URLS.APP_PATTERN}>

View File

@@ -0,0 +1,12 @@
type ConnectionData = {
screenName: string;
}
type Connection = {
id: string;
key: string;
data: ConnectionData;
verified: boolean;
};
export type { Connection, ConnectionData };