feat: introduce reconnect feature for connections

This commit is contained in:
Ali BARIN
2021-10-20 21:01:48 +02:00
parent 258caa81f0
commit ec4dd8a037
16 changed files with 75 additions and 41 deletions

View File

@@ -1,7 +1,6 @@
import TwitterApi from 'twitter-api-v2'; import TwitterApi from 'twitter-api-v2';
import App from '../../models/app'; import App from '../../models/app';
import Field from '../../types/field'; import Field from '../../types/field';
import appData from './info';
export default class Twitter { export default class Twitter {
client: any client: any
@@ -17,7 +16,7 @@ export default class Twitter {
}); });
this.connectionData = connectionData; this.connectionData = connectionData;
this.appData = appData; this.appData = App.findOneByKey('twitter');
} }
async createAuthLink() { async createAuthLink() {

View File

@@ -14,7 +14,7 @@ const createAuthLinkResolver = async (params: Params, req: RequestWithCurrentUse
const appClass = (await import(`../../apps/${connection.key}`)).default; const appClass = (await import(`../../apps/${connection.key}`)).default;
const appInstance = new appClass(connection.data) const appInstance = new appClass({ consumerKey: connection.data.consumerKey, consumerSecret: connection.data.consumerSecret });
const authLink = await appInstance.createAuthLink(); const authLink = await appInstance.createAuthLink();
await connection.$query().patch({ await connection.$query().patch({

View File

@@ -21,14 +21,17 @@ const updateConnectionResolver = async (params: Params, req: RequestWithCurrentU
} }
}) })
const appClass = (await import(`../../apps/${connection.key}`)).default; // Not every updateConnection mutation can verify credentials as some need to reconnect
try {
const appClass = (await import(`../../apps/${connection.key}`)).default;
const appInstance = new appClass(connection.data) const appInstance = new appClass(connection.data)
const verifiedCredentials = await appInstance.verifyCredentials(); const verifiedCredentials = await appInstance.verifyCredentials();
connection = await connection.$query().patchAndFetch({ connection = await connection.$query().patchAndFetch({
data: verifiedCredentials data: verifiedCredentials
}) })
} catch {}
return connection; return connection;
} }

View File

@@ -4,6 +4,12 @@ import graphQLSchema from '../graphql/graphql-schema'
const graphQLInstance = graphqlHTTP({ const graphQLInstance = graphqlHTTP({
schema: graphQLSchema, schema: graphQLSchema,
graphiql: true, graphiql: true,
customFormatErrorFn: (error) => ({
message: error.message,
locations: error.locations,
stack: error.stack ? error.stack.split('\n') : [],
path: error.path,
})
}) })
export default graphQLInstance; export default graphQLInstance;

View File

@@ -15,6 +15,7 @@ import { Form } from './style';
type AddAppConnectionProps = { type AddAppConnectionProps = {
onClose: () => void; onClose: () => void;
application: App; application: App;
connectionId?: string;
}; };
type Response = { type Response = {
@@ -22,8 +23,8 @@ type Response = {
} }
export default function AddAppConnection(props: AddAppConnectionProps){ export default function AddAppConnection(props: AddAppConnectionProps){
const { application, onClose } = props; const { application, connectionId, onClose } = props;
const { key, fields, authenticationSteps } = application; const { key, fields, authenticationSteps, reconnectionSteps } = application;
useEffect(() => { useEffect(() => {
if (window.opener) { if (window.opener) {
@@ -33,14 +34,21 @@ export default function AddAppConnection(props: AddAppConnectionProps){
}, []); }, []);
const submitHandler: SubmitHandler<FieldValues> = useCallback(async (data) => { const submitHandler: SubmitHandler<FieldValues> = useCallback(async (data) => {
const hasConnection = Boolean(connectionId);
const response: Response = { const response: Response = {
key, key,
connection: {
id: connectionId
},
fields: data, fields: data,
}; };
const steps = hasConnection ? reconnectionSteps : authenticationSteps;
let stepIndex = 0; let stepIndex = 0;
while (stepIndex < authenticationSteps.length) { while (stepIndex < steps.length) {
const step = authenticationSteps[stepIndex]; const step = steps[stepIndex];
const variables = computeAuthStepVariables(step, response); const variables = computeAuthStepVariables(step, response);
const stepResponse = await processStep(step, variables); const stepResponse = await processStep(step, variables);
@@ -51,7 +59,7 @@ export default function AddAppConnection(props: AddAppConnectionProps){
} }
onClose?.(); onClose?.();
}, [authenticationSteps, key, onClose]); }, [connectionId, key, reconnectionSteps, authenticationSteps, onClose]);
return ( return (
<Dialog open={true} onClose={onClose}> <Dialog open={true} onClose={onClose}>

View File

@@ -13,13 +13,14 @@ type Action = {
type ContextMenuProps = { type ContextMenuProps = {
appKey: string; appKey: string;
connectionId: string;
onClose: () => void; onClose: () => void;
onMenuItemClick: (event: React.MouseEvent, action: Action) => void; onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
anchorEl: PopoverProps['anchorEl']; anchorEl: PopoverProps['anchorEl'];
}; };
export default function ContextMenu(props: ContextMenuProps) { export default function ContextMenu(props: ContextMenuProps) {
const { appKey, onClose, onMenuItemClick, anchorEl } = props; const { appKey, connectionId, onClose, onMenuItemClick, anchorEl } = props;
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const createActionHandler = React.useCallback((action: Action) => { const createActionHandler = React.useCallback((action: Action) => {
@@ -37,7 +38,11 @@ export default function ContextMenu(props: ContextMenuProps) {
hideBackdrop={false} hideBackdrop={false}
anchorEl={anchorEl} anchorEl={anchorEl}
> >
<MenuItem component={Link} to={URLS.APP_FLOWS(appKey)} onClick={createActionHandler({ type: 'viewFlows' })}> <MenuItem
component={Link}
to={URLS.APP_FLOWS(appKey)}
onClick={createActionHandler({ type: 'viewFlows' })}
>
{formatMessage('connection.viewFlows')} {formatMessage('connection.viewFlows')}
</MenuItem> </MenuItem>
@@ -45,7 +50,11 @@ export default function ContextMenu(props: ContextMenuProps) {
{formatMessage('connection.testConnection')} {formatMessage('connection.testConnection')}
</MenuItem> </MenuItem>
<MenuItem onClick={createActionHandler({ type: 'reconnect' })}> <MenuItem
component={Link}
to={URLS.APP_RECONNECT_CONNECTION(appKey, connectionId)}
onClick={createActionHandler({ type: 'reconnect' })}
>
{formatMessage('connection.reconnect')} {formatMessage('connection.reconnect')}
</MenuItem> </MenuItem>

View File

@@ -19,11 +19,11 @@ type AppConnectionRowProps = {
const countTranslation = (value: React.ReactNode) => (<><strong>{value}</strong><br /></>); const countTranslation = (value: React.ReactNode) => (<><strong>{value}</strong><br /></>);
function AppConnectionRow(props: AppConnectionRowProps) { function AppConnectionRow(props: AppConnectionRowProps) {
const [testConnection, { data: testData, called: testCalled, loading: testLoading }] = useLazyQuery(TEST_CONNECTION); const [testConnection, { called: testCalled, loading: testLoading }] = useLazyQuery(TEST_CONNECTION);
const [deleteConnection] = useMutation(DELETE_CONNECTION); const [deleteConnection] = useMutation(DELETE_CONNECTION);
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { id, key, data } = props.connection; const { id, key, data, verified } = props.connection;
const contextButtonRef = React.useRef<SVGSVGElement | null>(null); const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null); const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
@@ -39,7 +39,7 @@ function AppConnectionRow(props: AppConnectionRowProps) {
variables: { id }, variables: { id },
update: (cache, mutationResult) => { update: (cache, mutationResult) => {
const connectionCacheId = cache.identify({ const connectionCacheId = cache.identify({
__typename: 'connection', __typename: 'Connection',
id, id,
}); });
@@ -65,7 +65,7 @@ function AppConnectionRow(props: AppConnectionRowProps) {
</Box> </Box>
<Box> <Box>
{testCalled && !testLoading && (testData ? 'yes' : 'no')} {testCalled && !testLoading && (verified ? 'yes' : 'no')}
</Box> </Box>
<Box sx={{ px: 2 }}> <Box sx={{ px: 2 }}>
@@ -83,6 +83,7 @@ function AppConnectionRow(props: AppConnectionRowProps) {
{anchorEl && <ConnectionContextMenu {anchorEl && <ConnectionContextMenu
appKey={key} appKey={key}
connectionId={id}
onClose={handleClose} onClose={handleClose}
onMenuItemClick={onContextMenuAction} onMenuItemClick={onContextMenuAction}
anchorEl={anchorEl} anchorEl={anchorEl}

View File

@@ -3,7 +3,6 @@ import { DrawerProps } from '@mui/material/Drawer';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List'; import List from '@mui/material/List';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import DashboardIcon from '@mui/icons-material/Dashboard';
import AppsIcon from '@mui/icons-material/Apps'; import AppsIcon from '@mui/icons-material/Apps';
import LanguageIcon from '@mui/icons-material/Language'; import LanguageIcon from '@mui/icons-material/Language';
import OfflineBoltIcon from '@mui/icons-material/OfflineBolt'; import OfflineBoltIcon from '@mui/icons-material/OfflineBolt';
@@ -29,12 +28,6 @@ export default function Drawer(props: DrawerProps) {
</HideOnScroll> </HideOnScroll>
<List> <List>
<ListItemLink
icon={<DashboardIcon />}
primary={formatMessage('drawer.dashboard')}
to={URLS.DASHBOARD}
/>
<ListItemLink <ListItemLink
icon={<OfflineBoltIcon />} icon={<OfflineBoltIcon />}
primary={formatMessage('drawer.flows')} primary={formatMessage('drawer.flows')}

View File

@@ -8,6 +8,8 @@ export const APP_CONNECTIONS = (appKey: string) => `/app/${appKey}/connections`;
export const APP_CONNECTIONS_PATTERN = '/app/:key/connections'; export const APP_CONNECTIONS_PATTERN = '/app/:key/connections';
export const APP_ADD_CONNECTION = (appKey: string) => `/app/${appKey}/connections/add`; export const APP_ADD_CONNECTION = (appKey: string) => `/app/${appKey}/connections/add`;
export const APP_ADD_CONNECTION_PATTERN = '/app/:key/connections/add'; export const APP_ADD_CONNECTION_PATTERN = '/app/:key/connections/add';
export const APP_RECONNECT_CONNECTION = (appKey: string, connectionId: string) => `/app/${appKey}/connections/${connectionId}/reconnect`;
export const APP_RECONNECT_CONNECTION_PATTERN = '/app/:key/connections/:connectionId/reconnect';
export const APP_FLOWS = (appKey: string) => `/app/${appKey}/flows`; export const APP_FLOWS = (appKey: string) => `/app/${appKey}/flows`;
export const APP_FLOWS_PATTERN = '/app/:key/flows'; export const APP_FLOWS_PATTERN = '/app/:key/flows';

View File

@@ -1,7 +1,7 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const CREATE_CONNECTION = gql` export const CREATE_CONNECTION = gql`
mutation CreateConnection($key: String!, $data: twitterCredentialInput!) { mutation CreateConnection($key: String!, $data: TwitterCredentialInput!) {
createConnection(key: $key, data: $data) { createConnection(key: $key, data: $data) {
id id
key key

View File

@@ -1,7 +1,7 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const UPDATE_CONNECTION = gql` export const UPDATE_CONNECTION = gql`
mutation UpdateConnection($id: String!, $data: twitterCredentialInput!) { mutation UpdateConnection($id: String!, $data: TwitterCredentialInput!) {
updateConnection(id: $id, data: $data) { updateConnection(id: $id, data: $data) {
id id
key key

View File

@@ -32,6 +32,19 @@ export const GET_APP = gql`
} }
} }
} }
reconnectionSteps {
step
type
name
fields {
name
value
fields {
name
value
}
}
}
connections { connections {
id id
} }

View File

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

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { Link, Route, Redirect, Switch, useParams, useRouteMatch, useHistory } from 'react-router-dom'; import { Link, Route, Redirect, Switch, RouteComponentProps, useParams, useRouteMatch, useHistory } from 'react-router-dom';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@@ -20,6 +20,7 @@ import PageTitle from 'components/PageTitle';
type ApplicationParams = { type ApplicationParams = {
key: string; key: string;
connectionId?: string;
}; };
export default function Application() { export default function Application() {
@@ -95,9 +96,9 @@ export default function Application() {
</Container> </Container>
</Box> </Box>
<Route exact path={URLS.APP_ADD_CONNECTION_PATTERN}> <Route exact path={[URLS.APP_RECONNECT_CONNECTION_PATTERN, URLS.APP_ADD_CONNECTION_PATTERN]} render={({ match }: RouteComponentProps<ApplicationParams>) => (
<AddAppConnection onClose={goToApplicationPage} application={app} /> <AddAppConnection onClose={goToApplicationPage} application={app} connectionId={match.params.connectionId} />
</Route> )} />
</> </>
); );
}; };

View File

@@ -1,5 +1,4 @@
import { Route, Switch, Redirect } from "react-router"; import { Route, Switch, Redirect } from "react-router";
import Dashboard from 'pages/Dashboard';
import Applications from 'pages/Applications'; import Applications from 'pages/Applications';
import Application from 'pages/Application'; import Application from 'pages/Application';
import Flows from 'pages/Flows'; import Flows from 'pages/Flows';
@@ -8,10 +7,6 @@ import * as URLS from 'config/urls';
export default ( export default (
<Switch> <Switch>
<Route path={URLS.DASHBOARD}>
<Dashboard />
</Route>
<Route path={URLS.FLOWS}> <Route path={URLS.FLOWS}>
<Flows /> <Flows />
</Route> </Route>
@@ -29,7 +24,7 @@ export default (
</Route> </Route>
<Route exact path="/"> <Route exact path="/">
<Redirect to={URLS.DASHBOARD} /> <Redirect to={URLS.FLOWS} />
</Route> </Route>
<Route> <Route>

View File

@@ -19,6 +19,7 @@ type App = {
primaryColor: string; primaryColor: string;
fields: AppFields[]; fields: AppFields[];
authenticationSteps: any[]; authenticationSteps: any[];
reconnectionSteps: any[];
}; };
export type { App, AppFields }; export type { App, AppFields };