Skip to content

Commit

Permalink
feat(backoffice-v2 and workflows-service): implement websocket in bac…
Browse files Browse the repository at this point in the history
…koffice-v2 & workflows-service

installed react-use-websocket package for implementing websockets in backoffice-v2, connect to
websocket and wait for update messages instead of polling. if disconnected start polling again.
workflows-service send api calls to websocket server to send update messages to all clients

BREAKING CHANGE: backoffice-v2 connects to websocket and if fails keep polling in order to present
updated data

feat #534
  • Loading branch information
teselil committed Jun 28, 2023
1 parent d5e8458 commit 882fcfa
Show file tree
Hide file tree
Showing 28 changed files with 271 additions and 214 deletions.
1 change: 1 addition & 0 deletions apps/backoffice-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"react-image-crop": "^10.0.9",
"react-leaflet": "^4.2.1",
"react-router-dom": "^6.11.2",
"react-use-websocket": "^4.3.1",
"react-zoom-pan-pinch": "^3.0.8",
"tailwind-merge": "^1.10.0",
"tailwindcss-animate": "^1.0.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { TRoutes } from '../../../../Router/types';
import { CheckSquare } from 'lucide-react';
import { useSearchParamsByEntity } from '../../../hooks/useSearchParamsByEntity/useSearchParamsByEntity';
import { useSelectEntityFilterOnMount } from '../../../../domains/entities/hooks/useSelectEntityFilterOnMount/useSelectEntityFilterOnMount';
import useWebSocket, { ReadyState } from 'react-use-websocket';
import { queryClient } from '../../../../lib/react-query/query-client';

/**
* @description A nav element which wraps {@link NavItem} components of the app's routes. Supports nested routes.
Expand All @@ -15,7 +17,12 @@ import { useSelectEntityFilterOnMount } from '../../../../domains/entities/hooks
* @constructor
*/
export const Navbar: FunctionComponent = () => {
const { data: filters } = useFiltersQuery();
const { readyState } = useWebSocket('ws://localhost:3500/?testParams=55', {
share: true,
shouldReconnect: () => true,
});

const { data: filters } = useFiltersQuery(readyState === ReadyState.OPEN);
const [searchParams] = useSearchParamsByEntity();
const navItems = [
// {
Expand All @@ -26,7 +33,7 @@ export const Navbar: FunctionComponent = () => {
// },
] satisfies TRoutes;

useSelectEntityFilterOnMount();
useSelectEntityFilterOnMount(readyState === ReadyState.OPEN);

return (
<nav>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Header } from '../../../../common/components/organisms/Header';
import { FunctionComponentWithChildren } from '../../../../common/types';
import { useSelectEntityFilterOnMount } from '../../../entities/hooks/useSelectEntityFilterOnMount/useSelectEntityFilterOnMount';
import useWebSocket, { ReadyState } from 'react-use-websocket';

export const AuthenticatedLayout: FunctionComponentWithChildren = ({ children }) => {
const { readyState } = useWebSocket('ws://localhost:3500/?testParams=55', {
share: true,
shouldReconnect: () => true,
});
// Should only be uncommented once `useAuthRedirects` is no longer in use in `AuthProvider`
// useAuthenticatedLayout();
useSelectEntityFilterOnMount();
useSelectEntityFilterOnMount(readyState == ReadyState.OPEN);

return (
<div className="drawer-mobile drawer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { useEffect } from 'react';
import { useSearchParamsByEntity } from '../../../../common/hooks/useSearchParamsByEntity/useSearchParamsByEntity';
import { useNavigate, useParams } from 'react-router-dom';

export const useSelectEntityFilterOnMount = () => {
const { data: filters } = useFiltersQuery();
export const useSelectEntityFilterOnMount = (websocketConnectionIsOpen: boolean) => {
const { data: filters } = useFiltersQuery(websocketConnectionIsOpen);
const { locale } = useParams();
const [{ entity, filterId }, setSearchParams] = useSearchParamsByEntity();
const navigate = useNavigate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ import { useSearchParamsByEntity } from '../../../../common/hooks/useSearchParam
/**
* @description Sets the selected end user to the first end user in the array on mount if no user is currently selected. Returns the select end user handler.
*/
export const useSelectEntityOnMount = () => {
export const useSelectEntityOnMount = (websocketConnectionIsOpen: boolean) => {
const { entityId } = useParams();
const [{ filterId, filter, sortBy, sortDir, page, pageSize }] = useSearchParamsByEntity();
const { data } = useWorkflowsQuery({ filterId, filter, sortBy, sortDir, page, pageSize });
const { data } = useWorkflowsQuery({
filterId,
filter,
sortBy,
sortDir,
page,
pageSize,
websocketConnectionIsOpen,
});
const { data: workflows } = data || { data: [] };
const onSelectEntity = useSelectEntity();
const entity = useFilterEntity();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { filtersQueryKeys } from '../../../query-keys';

export const useFiltersQuery = () => {
export const useFiltersQuery = (websocketConnectionIsOpen: boolean) => {
return useQuery({
...filtersQueryKeys.list(),
staleTime: 1_000_000,
refetchInterval: 1_000_000,
refetchInterval: () => (websocketConnectionIsOpen ? false : 1_000_000),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { isString } from '../../../../../common/utils/is-string/is-string';
import { storageQueryKeys } from '../../../query-keys';
import { useIsAuthenticated } from '../../../../auth/context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated';

export const useStorageFilesQuery = (fileIds: Array<string>) => {
export const useStorageFilesQuery = (
fileIds: Array<string>,
websocketConnectionIsOpen: boolean,
) => {
const isAuthenticated = useIsAuthenticated();

return useQueries({
Expand All @@ -12,7 +15,7 @@ export const useStorageFilesQuery = (fileIds: Array<string>) => {
...storageQueryKeys.fileById(fileId),
enabled: isString(fileId) && !!fileId?.length && isAuthenticated,
staleTime: 100_000,
refetchInterval: 100_000,
refetchInterval: () => (websocketConnectionIsOpen ? false : 100_000),
})) ?? [],
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { workflowsQueryKeys } from '../../../query-keys';
export const useWorkflowQuery = ({
workflowId,
filterId,
websocketConnectionIsOpen,
}: {
workflowId: string;
filterId: string;
websocketConnectionIsOpen: boolean;
}) => {
return useQuery({
...workflowsQueryKeys.byId({ workflowId, filterId }),
enabled: !!filterId && !!workflowId,
staleTime: 10_000,
staleTime: 100_000,
refetchInterval: () => (websocketConnectionIsOpen ? false : 10_000),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ export const useWorkflowsQuery = ({
sortDir,
page,
pageSize,
websocketConnectionIsOpen,
filter,
}: {
filterId: string;
sortBy: string;
sortDir: string;
page: number;
pageSize: number;
websocketConnectionIsOpen: boolean;
filter: Record<string, unknown>;
}) => {
const isAuthenticated = useIsAuthenticated();
Expand All @@ -24,5 +26,6 @@ export const useWorkflowsQuery = ({
...workflowsQueryKeys.list({ filterId, filter, sortBy, sortDir, page, pageSize }),
enabled: !!filterId && isAuthenticated && !!sortBy && !!sortDir && !!page && !!pageSize,
staleTime: 100_000,
refetchInterval: () => (websocketConnectionIsOpen ? false : 100_000),
});
};
8 changes: 7 additions & 1 deletion apps/backoffice-v2/src/pages/Entities/Entities.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import { useEntities } from './hooks/useEntities/useEntities';
import { Case } from '../Entity/components/Case/Case';
import { MotionScrollArea } from '../../common/components/molecules/MotionScrollArea/MotionScrollArea';
import { FunctionComponent } from 'react';
import useWebSocket, { ReadyState } from 'react-use-websocket';

export const Entities: FunctionComponent = () => {
const { readyState } = useWebSocket('ws://localhost:3500/?testParams=55', {
share: true,
shouldReconnect: () => true,
});

const {
onPaginate,
onSearch,
Expand All @@ -20,7 +26,7 @@ export const Entities: FunctionComponent = () => {
totalPages,
skeletonEntities,
entity,
} = useEntities();
} = useEntities(readyState === ReadyState.OPEN);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { useFilterEntity } from '../../../../domains/entities/hooks/useFilterEnt
import { useSelectEntityOnMount } from '../../../../domains/entities/hooks/useSelectEntityOnMount/useSelectEntityOnMount';
import { useWorkflowsQuery } from '../../../../domains/workflows/hooks/queries/useWorkflowsQuery/useWorkflowsQuery';
import { useSearchParamsByEntity } from '../../../../common/hooks/useSearchParamsByEntity/useSearchParamsByEntity';
import useWebSocket from 'react-use-websocket';

export const useEntities = () => {
export const useEntities = (websocketConnectionIsOpen: boolean) => {
const [{ filterId, filter, sortBy, sortDir, page, pageSize }, setSearchParams] =
useSearchParamsByEntity();
const { data, isLoading } = useWorkflowsQuery({
Expand All @@ -15,6 +16,7 @@ export const useEntities = () => {
sortBy,
sortDir,
page,
websocketConnectionIsOpen,
pageSize,
});
const {
Expand Down Expand Up @@ -83,7 +85,7 @@ export const useEntities = () => {
);
const skeletonEntities = createArrayOfNumbers(3);

useSelectEntityOnMount();
useSelectEntityOnMount(websocketConnectionIsOpen);

return {
onPaginate,
Expand Down
10 changes: 9 additions & 1 deletion apps/backoffice-v2/src/pages/Entity/Entity.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import { useEntity } from './hooks/useEntity/useEntity';
import { ctw } from '../../common/utils/ctw/ctw';
import { Card } from '../../common/components/atoms/Card/Card';
import { CardContent } from '../../common/components/atoms/Card/Card.Content';
import useWebSocket, { ReadyState } from 'react-use-websocket';

export const Entity = () => {
const { workflow, selectedEntity, tasks, cells, isLoading } = useEntity();
const { readyState } = useWebSocket('ws://localhost:3500/?testParams=55', {
share: true,
shouldReconnect: () => true,
});

const { workflow, selectedEntity, tasks, cells, isLoading } = useEntity(
readyState === ReadyState.OPEN,
);

// Selected entity
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ import { SelectValue } from '../../../../common/components/atoms/Select/Select.V
import { Input } from '../../../../common/components/atoms/Input/Input';
import { DialogTrigger } from '../../../../common/components/organisms/Dialog/Dialog.Trigger';
import { useCallToActionLogic } from './hooks/useCallToActionLogic/useCallToActionLogic';
import useWebSocket, { ReadyState } from 'react-use-websocket';

export const CallToAction: FunctionComponent<ICallToActionProps> = ({ value, data }) => {
const { readyState } = useWebSocket('ws://localhost:3500/?testParams=55', {
share: true,
shouldReconnect: () => true,
});

const {
onMutateUpdateWorkflowById,
isLoadingUpdateWorkflowById,
Expand All @@ -26,7 +32,7 @@ export const CallToAction: FunctionComponent<ICallToActionProps> = ({ value, dat
reasons,
reason,
onReasonChange,
} = useCallToActionLogic();
} = useCallToActionLogic(readyState === ReadyState.OPEN);

return value === 'Reject' ? (
<Dialog>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { useUpdateWorkflowByIdMutation } from '../../../../../../domains/workflo
import toast from 'react-hot-toast';
import { useCallback, useState } from 'react';

export const useCallToActionLogic = () => {
export const useCallToActionLogic = (websocketConnectionIsOpen: boolean) => {
const { entityId } = useParams();
const filterId = useFilterId();
const { data: workflow } = useWorkflowQuery({ workflowId: entityId, filterId });
const { data: workflow } = useWorkflowQuery({
workflowId: entityId,
filterId,
websocketConnectionIsOpen,
});
const { data: session } = useAuthenticatedUserQuery();
const caseState = useCaseState(session?.user, workflow);
const { mutate: mutateUpdateWorkflowById, isLoading: isLoadingUpdateWorkflowById } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { SelectItem } from '../../../../common/components/atoms/Select/Select.It
import { SelectContent } from '../../../../common/components/atoms/Select/Select.Content';
import { SelectTrigger } from '../../../../common/components/atoms/Select/Select.Trigger';
import { SelectValue } from '../../../../common/components/atoms/Select/Select.Value';
import useWebSocket, { ReadyState } from 'react-use-websocket';

/**
* @description To be used by {@link Case}. Displays the entity's full name, avatar, and handles the reject/approve mutation.
Expand All @@ -49,6 +50,10 @@ export const Actions: FunctionComponent<IActionsProps> = ({
fullName,
showResolutionButtons = true,
}) => {
const { readyState } = useWebSocket('ws://localhost:3500/?testParams=55', {
share: true,
shouldReconnect: () => true,
});
const {
onMutateApproveEntity,
onMutateRejectEntity,
Expand All @@ -71,7 +76,11 @@ export const Actions: FunctionComponent<IActionsProps> = ({
isActionButtonDisabled,
onTriggerAssignToMe,
isAssignedToMe,
} = useActions({ workflowId: id, fullName });
} = useActions({
workflowId: id,
fullName,
websocketConnectionIsOpen: readyState === ReadyState.OPEN,
});

return (
<div className={`sticky top-0 z-50 col-span-2 bg-base-100 px-4 pt-4`}>
Expand Down Expand Up @@ -112,7 +121,7 @@ export const Actions: FunctionComponent<IActionsProps> = ({
</h2>
</div>
{showResolutionButtons && (
<div className={`flex items-center space-x-6 pe-[3.35rem]`}>
<div className={`pe-[3.35rem] flex items-center space-x-6`}>
<Button
className={ctw({
// loading: debouncedIsLoadingRejectEntity,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface IUseActions {
workflowId: string;
fullName: string;
websocketConnectionIsOpen: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export const ResubmissionReason = {
FACE_IS_NOT_MATCHING: 'FACE_IS_NOT_MATCHING',
} as const;

export const useActions = ({ workflowId, fullName }: IUseActions) => {
export const useActions = ({ workflowId, fullName, websocketConnectionIsOpen }: IUseActions) => {
const onSelectNextEntity = useSelectNextEntity();
const filterId = useFilterId();
const { data: workflow } = useWorkflowQuery({ workflowId, filterId });
const { data: workflow } = useWorkflowQuery({ workflowId, filterId, websocketConnectionIsOpen });
const { mutate: mutateApproveEntity, isLoading: isLoadingApproveEntity } =
useApproveEntityMutation({
workflowId: workflowId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,21 @@ import { useParams } from 'react-router-dom';
import { IDetailsProps } from './interfaces';
import { useWorkflowQuery } from '../../../../domains/workflows/hooks/queries/useWorkflowQuery/useWorkflowQuery';
import { useFilterId } from '../../../../common/hooks/useFilterId/useFilterId';
import useWebSocket, { ReadyState } from 'react-use-websocket';

export const Details: FunctionComponent<IDetailsProps> = ({ id, value }) => {
const { readyState } = useWebSocket('ws://localhost:3500/?testParams=55', {
share: true,
shouldReconnect: () => true,
});

const { entityId } = useParams();
const filterId = useFilterId();
const { data: workflow } = useWorkflowQuery({ workflowId: entityId, filterId });
const { data: workflow } = useWorkflowQuery({
workflowId: entityId,
filterId,
websocketConnectionIsOpen: readyState === ReadyState.OPEN,
});

if (!value.data?.length) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,20 @@ import {
import { getDocumentsByCountry } from '@ballerine/common';
import { getAddressDeep } from './utils/get-address-deep/get-address-deep';

export const useEntity = () => {
export const useEntity = (websocketConnectionIsOpen: boolean) => {
const { entityId } = useParams();
const filterId = useFilterId();

const { data: workflow, isLoading } = useWorkflowQuery({ workflowId: entityId, filterId });
const { data: workflow, isLoading } = useWorkflowQuery({
workflowId: entityId,
filterId,
websocketConnectionIsOpen,
});
const docsData = useStorageFilesQuery(
workflow.context.documents?.flatMap(({ pages }) =>
pages?.map(({ ballerineFileId }) => ballerineFileId),
),
websocketConnectionIsOpen,
);

const results = [];
Expand Down
Loading

0 comments on commit 882fcfa

Please sign in to comment.