feat: introduce reconnect feature for connections
This commit is contained in:
@@ -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() {
|
||||||
|
@@ -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({
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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}>
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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}
|
||||||
|
@@ -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')}
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -32,6 +32,19 @@ export const GET_APP = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
reconnectionSteps {
|
||||||
|
step
|
||||||
|
type
|
||||||
|
name
|
||||||
|
fields {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
fields {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
connections {
|
connections {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
@@ -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>
|
)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
||||||
|
@@ -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 };
|
||||||
|
Reference in New Issue
Block a user