feat: add app connections w/ testing and deleting functions
This commit is contained in:
@@ -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() {
|
||||
|
@@ -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 = {
|
||||
|
@@ -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 = {
|
||||
|
@@ -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;
|
||||
|
@@ -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({
|
||||
|
@@ -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) },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -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 }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
94
packages/web/src/components/AppConnectionRow/index.tsx
Normal file
94
packages/web/src/components/AppConnectionRow/index.tsx
Normal 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;
|
17
packages/web/src/components/AppConnectionRow/style.ts
Normal file
17
packages/web/src/components/AppConnectionRow/style.ts
Normal 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',
|
||||
}));
|
23
packages/web/src/components/AppConnections/index.tsx
Normal file
23
packages/web/src/components/AppConnections/index.tsx
Normal 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} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
};
|
@@ -19,12 +19,10 @@ export default function InputCreator(props: InputCreatorProps) {
|
||||
const {
|
||||
key: name,
|
||||
label,
|
||||
type,
|
||||
required,
|
||||
readOnly,
|
||||
value,
|
||||
description,
|
||||
docUrl,
|
||||
clickToCopy,
|
||||
} = schema;
|
||||
|
||||
|
@@ -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 (
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
7
packages/web/src/graphql/mutations/delete-connection.ts
Normal file
7
packages/web/src/graphql/mutations/delete-connection.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_CONNECTION = gql`
|
||||
mutation DeleteConnection($id: String!) {
|
||||
deleteConnection(id: $id)
|
||||
}
|
||||
`;
|
@@ -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,
|
||||
};
|
||||
|
||||
|
@@ -6,6 +6,9 @@ export const UPDATE_CONNECTION = gql`
|
||||
id
|
||||
key
|
||||
verified
|
||||
data {
|
||||
screenName
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
17
packages/web/src/graphql/queries/get-app-connections.ts
Normal file
17
packages/web/src/graphql/queries/get-app-connections.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@@ -32,6 +32,9 @@ export const GET_APP = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
connections {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -19,6 +19,9 @@ export const GET_APPS = gql`
|
||||
docUrl
|
||||
clickToCopy
|
||||
}
|
||||
connections {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
7
packages/web/src/graphql/queries/test-connection.ts
Normal file
7
packages/web/src/graphql/queries/test-connection.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const TEST_CONNECTION = gql`
|
||||
query TestConnection($id: String!) {
|
||||
testConnection(id: $id)
|
||||
}
|
||||
`;
|
@@ -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"
|
||||
}
|
@@ -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}>
|
||||
|
12
packages/web/src/types/connection.ts
Normal file
12
packages/web/src/types/connection.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
type ConnectionData = {
|
||||
screenName: string;
|
||||
}
|
||||
|
||||
type Connection = {
|
||||
id: string;
|
||||
key: string;
|
||||
data: ConnectionData;
|
||||
verified: boolean;
|
||||
};
|
||||
|
||||
export type { Connection, ConnectionData };
|
Reference in New Issue
Block a user