Merged in feature/IO-2924-Refactor-Production-board-to-use-Socket-Provider (pull request #1786)
Feature/IO-2924 Refactor Production board to use Socket Provider Approved-by: Patrick Fic
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useMemo, useRef } from "react";
|
import React, { useContext, useEffect, useMemo, useRef } from "react";
|
||||||
import { useQuery, useSubscription } from "@apollo/client";
|
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import {
|
||||||
|
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
QUERY_JOBS_IN_PRODUCTION,
|
QUERY_JOBS_IN_PRODUCTION,
|
||||||
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
||||||
@@ -10,6 +11,8 @@ import {
|
|||||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -17,7 +20,17 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
||||||
const fired = useRef(false); // useRef to keep track of whether the subscription fired
|
const fired = useRef(false);
|
||||||
|
const client = useApolloClient();
|
||||||
|
const { socket } = useContext(SocketContext); // Get the socket from context
|
||||||
|
|
||||||
|
const {
|
||||||
|
treatments: { Websocket_Production }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["Websocket_Production"],
|
||||||
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
const combinedStatuses = useMemo(
|
const combinedStatuses = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -34,9 +47,12 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
|
|||||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const subscriptionEnabled = Websocket_Production?.treatment === "on";
|
||||||
|
|
||||||
const { data: updatedJobs } = useSubscription(
|
const { data: updatedJobs } = useSubscription(
|
||||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||||
{
|
{
|
||||||
|
skip: !subscriptionEnabled,
|
||||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -46,22 +62,82 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
|
|||||||
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!updatedJobs) {
|
if (subscriptionEnabled) {
|
||||||
|
if (!updatedJobs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!fired.current) {
|
||||||
|
fired.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||||
|
}
|
||||||
|
}, [updatedJobs, refetch, subscriptionEnabled]);
|
||||||
|
|
||||||
|
// Socket.IO implementation for users with Split treatment "off"
|
||||||
|
useEffect(() => {
|
||||||
|
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!fired.current) {
|
|
||||||
fired.current = true;
|
const handleJobUpdates = async (jobChangedData) => {
|
||||||
return;
|
const jobId = jobChangedData.id;
|
||||||
}
|
|
||||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
|
||||||
}, [updatedJobs, refetch]);
|
const existingJobsCache = client.readQuery({
|
||||||
|
query: QUERY_JOBS_IN_PRODUCTION
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingJobs = existingJobsCache?.jobs || [];
|
||||||
|
|
||||||
|
// Check if the job already exists in the cached jobs
|
||||||
|
const existingJob = existingJobs.find((job) => job.id === jobId);
|
||||||
|
|
||||||
|
if (existingJob) {
|
||||||
|
// If the job exists, we update the cache without making any additional queries
|
||||||
|
client.writeQuery({
|
||||||
|
query: QUERY_JOBS_IN_PRODUCTION,
|
||||||
|
data: {
|
||||||
|
jobs: existingJobs.map((job) =>
|
||||||
|
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If the job doesn't exist, fetch it from the server and then add it to the cache
|
||||||
|
try {
|
||||||
|
const { data: jobData } = await client.query({
|
||||||
|
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
|
variables: { id: jobId },
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the job to the existing cached jobs
|
||||||
|
client.writeQuery({
|
||||||
|
query: QUERY_JOBS_IN_PRODUCTION,
|
||||||
|
data: {
|
||||||
|
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching job ${jobId}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for 'job-changed' events
|
||||||
|
socket.on("production-job-updated", handleJobUpdates);
|
||||||
|
|
||||||
|
// Clean up on unmount or when dependencies change
|
||||||
|
return () => {
|
||||||
|
socket.off("production-job-updated", handleJobUpdates);
|
||||||
|
};
|
||||||
|
}, [subscriptionEnabled, socket, bodyshop, data, client]);
|
||||||
|
|
||||||
const filteredAssociationSettings = useMemo(() => {
|
const filteredAssociationSettings = useMemo(() => {
|
||||||
return associationSettings?.associations[0] || null;
|
return associationSettings?.associations[0] || null;
|
||||||
}, [associationSettings]);
|
}, [associationSettings?.associations]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductionBoardKanbanComponent
|
<ProductionBoardKanbanComponent
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||||
@@ -9,19 +9,42 @@ import {
|
|||||||
} from "../../graphql/jobs.queries";
|
} from "../../graphql/jobs.queries";
|
||||||
import ProductionListTable from "./production-list-table.component";
|
import ProductionListTable from "./production-list-table.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
export default function ProductionListTableContainer({ subscriptionType = "direct" }) {
|
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||||
|
const client = useApolloClient();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
const [joblist, setJoblist] = useState([]);
|
||||||
|
|
||||||
|
// Get Split treatment
|
||||||
|
const {
|
||||||
|
treatments: { Websocket_Production }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["Websocket_Production"],
|
||||||
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine if subscription is enabled
|
||||||
|
const subscriptionEnabled = Websocket_Production?.treatment === "on";
|
||||||
|
|
||||||
|
// Use GraphQL query
|
||||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||||
pollInterval: 3600000,
|
pollInterval: 3600000,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
const client = useApolloClient();
|
|
||||||
const [joblist, setJoblist] = useState([]);
|
// Use GraphQL subscription when subscription is enabled
|
||||||
const { data: updatedJobs } = useSubscription(
|
const { data: updatedJobs } = useSubscription(
|
||||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION
|
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||||
|
{
|
||||||
|
skip: !subscriptionEnabled
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update joblist when data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!(data && data.jobs)) return;
|
if (!(data && data.jobs)) return;
|
||||||
setJoblist(
|
setJoblist(
|
||||||
@@ -31,34 +54,99 @@ export default function ProductionListTableContainer({ subscriptionType = "direc
|
|||||||
);
|
);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// Handle updates from GraphQL subscription
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!updatedJobs || joblist.length === 0) return;
|
if (subscriptionEnabled) {
|
||||||
|
if (!updatedJobs || joblist.length === 0) return;
|
||||||
|
|
||||||
const jobDiff = _.differenceWith(
|
const jobDiff = _.differenceWith(
|
||||||
joblist,
|
joblist,
|
||||||
updatedJobs.jobs,
|
updatedJobs.jobs,
|
||||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
||||||
);
|
);
|
||||||
|
|
||||||
if (jobDiff.length > 1) {
|
if (jobDiff.length > 1) {
|
||||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
||||||
} else if (jobDiff.length === 1) {
|
} else if (jobDiff.length === 1) {
|
||||||
jobDiff.forEach((job) => {
|
jobDiff.forEach((job) => {
|
||||||
getUpdatedJobData(job.id);
|
getUpdatedJobData(job.id);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setJoblist(updatedJobs.jobs);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [updatedJobs, subscriptionEnabled]);
|
||||||
|
|
||||||
|
// Handle updates from Socket.IO when subscription is disabled
|
||||||
|
useEffect(() => {
|
||||||
|
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setJoblist(updatedJobs.jobs);
|
const handleJobUpdates = async (jobChangedData) => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const jobId = jobChangedData.id;
|
||||||
}, [updatedJobs]);
|
|
||||||
|
|
||||||
|
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
|
||||||
|
const existingJobsCache = client.readQuery({
|
||||||
|
query: QUERY_JOBS_IN_PRODUCTION
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingJobs = existingJobsCache?.jobs || [];
|
||||||
|
|
||||||
|
// Check if the job already exists in the cached jobs
|
||||||
|
const existingJob = existingJobs.find((job) => job.id === jobId);
|
||||||
|
|
||||||
|
if (existingJob) {
|
||||||
|
// If the job exists, we update the cache without making any additional queries
|
||||||
|
client.writeQuery({
|
||||||
|
query: QUERY_JOBS_IN_PRODUCTION,
|
||||||
|
data: {
|
||||||
|
jobs: existingJobs.map((job) =>
|
||||||
|
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If the job doesn't exist, fetch it from the server and then add it to the cache
|
||||||
|
try {
|
||||||
|
const { data: jobData } = await client.query({
|
||||||
|
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
|
variables: { id: jobId },
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the job to the existing cached jobs
|
||||||
|
client.writeQuery({
|
||||||
|
query: QUERY_JOBS_IN_PRODUCTION,
|
||||||
|
data: {
|
||||||
|
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching job ${jobId}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for 'production-job-updated' events
|
||||||
|
socket.on("production-job-updated", handleJobUpdates);
|
||||||
|
|
||||||
|
// Clean up on unmount or when dependencies change
|
||||||
|
return () => {
|
||||||
|
socket.off("production-job-updated", handleJobUpdates);
|
||||||
|
};
|
||||||
|
}, [subscriptionEnabled, socket, bodyshop, client]);
|
||||||
|
|
||||||
|
// Functions to fetch updated job data
|
||||||
const getUpdatedJobData = async (jobId) => {
|
const getUpdatedJobData = async (jobId) => {
|
||||||
client.query({
|
await client.query({
|
||||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||||
variables: { id: jobId }
|
variables: { id: jobId },
|
||||||
|
fetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const getUpdatedJobsData = async (jobIds) => {
|
const getUpdatedJobsData = (jobIds) => {
|
||||||
client.query({
|
client.query({
|
||||||
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||||
variables: { ids: jobIds }
|
variables: { ids: jobIds }
|
||||||
|
|||||||
@@ -1,83 +1,83 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import SocketIO from "socket.io-client";
|
import SocketIO from "socket.io-client";
|
||||||
import { auth } from "../../firebase/firebase.utils";
|
import { auth } from "../../firebase/firebase.utils";
|
||||||
|
|
||||||
const useSocket = (bodyshop) => {
|
const useSocket = (bodyshop) => {
|
||||||
const [socket, setSocket] = useState(null);
|
const socketRef = useRef(null);
|
||||||
const [clientId, setClientId] = useState(null);
|
const [clientId, setClientId] = useState(null);
|
||||||
const [token, setToken] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Listener for token changes
|
|
||||||
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
const newToken = await user.getIdToken();
|
const newToken = await user.getIdToken();
|
||||||
setToken(newToken);
|
|
||||||
|
if (socketRef.current) {
|
||||||
|
// Send new token to server
|
||||||
|
socketRef.current.emit("update-token", newToken);
|
||||||
|
} else if (bodyshop && bodyshop.id) {
|
||||||
|
// Initialize the socket
|
||||||
|
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
|
||||||
|
|
||||||
|
const socketInstance = SocketIO(endpoint, {
|
||||||
|
path: "/wss",
|
||||||
|
withCredentials: true,
|
||||||
|
auth: { token: newToken },
|
||||||
|
reconnectionAttempts: Infinity,
|
||||||
|
reconnectionDelay: 2000,
|
||||||
|
reconnectionDelayMax: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current = socketInstance;
|
||||||
|
|
||||||
|
const handleBodyshopMessage = (message) => {
|
||||||
|
if (!import.meta.env.DEV) return;
|
||||||
|
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnect = () => {
|
||||||
|
console.log("Socket connected:", socketInstance.id);
|
||||||
|
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||||
|
setClientId(socketInstance.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReconnect = (attempt) => {
|
||||||
|
console.log(`Socket reconnected after ${attempt} attempts`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnectionError = (err) => {
|
||||||
|
console.error("Socket connection error:", err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
console.log("Socket disconnected");
|
||||||
|
};
|
||||||
|
|
||||||
|
socketInstance.on("connect", handleConnect);
|
||||||
|
socketInstance.on("reconnect", handleReconnect);
|
||||||
|
socketInstance.on("connect_error", handleConnectionError);
|
||||||
|
socketInstance.on("disconnect", handleDisconnect);
|
||||||
|
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setToken(null);
|
// User is not authenticated
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
socketRef.current = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up the listener on unmount
|
// Clean up the listener on unmount
|
||||||
return () => unsubscribe();
|
return () => {
|
||||||
}, []);
|
unsubscribe();
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
socketRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [bodyshop]);
|
||||||
|
|
||||||
useEffect(() => {
|
return { socket: socketRef.current, clientId };
|
||||||
if (bodyshop && bodyshop.id && token) {
|
|
||||||
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
|
|
||||||
|
|
||||||
const socketInstance = SocketIO(endpoint, {
|
|
||||||
path: "/wss",
|
|
||||||
withCredentials: true,
|
|
||||||
auth: { token }, // Use the current token
|
|
||||||
reconnectionAttempts: Infinity,
|
|
||||||
reconnectionDelay: 2000,
|
|
||||||
reconnectionDelayMax: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
setSocket(socketInstance);
|
|
||||||
|
|
||||||
const handleBodyshopMessage = (message) => {
|
|
||||||
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConnect = () => {
|
|
||||||
console.log("Socket connected:", socketInstance.id);
|
|
||||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
|
||||||
setClientId(socketInstance.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReconnect = (attempt) => {
|
|
||||||
console.log(`Socket reconnected after ${attempt} attempts`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConnectionError = (err) => {
|
|
||||||
console.error("Socket connection error:", err);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
|
||||||
console.log("Socket disconnected");
|
|
||||||
};
|
|
||||||
|
|
||||||
socketInstance.on("connect", handleConnect);
|
|
||||||
socketInstance.on("reconnect", handleReconnect);
|
|
||||||
socketInstance.on("connect_error", handleConnectionError);
|
|
||||||
socketInstance.on("disconnect", handleDisconnect);
|
|
||||||
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socketInstance.emit("leave-bodyshop-room", bodyshop.id);
|
|
||||||
socketInstance.off("connect", handleConnect);
|
|
||||||
socketInstance.off("reconnect", handleReconnect);
|
|
||||||
socketInstance.off("connect_error", handleConnectionError);
|
|
||||||
socketInstance.off("disconnect", handleDisconnect);
|
|
||||||
socketInstance.off("bodyshop-message", handleBodyshopMessage);
|
|
||||||
socketInstance.disconnect();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [bodyshop, token]); // Include 'token' in dependencies to re-run effect when it changes
|
|
||||||
|
|
||||||
return { socket, clientId };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useSocket;
|
export default useSocket;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function ProductionListComponent({ bodyshop }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteUpsertModal />
|
<NoteUpsertModal />
|
||||||
<ProductionListTable subscriptionType={Production_Use_View.treatment} />
|
<ProductionListTable bodyshop={bodyshop} subscriptionType={Production_Use_View.treatment} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const { instrument, RedisStore } = require("@socket.io/admin-ui");
|
|||||||
|
|
||||||
const { isString, isEmpty } = require("lodash");
|
const { isString, isEmpty } = require("lodash");
|
||||||
const applyRedisHelpers = require("./server/utils/redisHelpers");
|
const applyRedisHelpers = require("./server/utils/redisHelpers");
|
||||||
|
const applyIOHelpers = require("./server/utils/ioHelpers");
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
@@ -192,13 +193,14 @@ const main = async () => {
|
|||||||
|
|
||||||
const { pubClient, ioRedis } = await applySocketIO(server, app);
|
const { pubClient, ioRedis } = await applySocketIO(server, app);
|
||||||
const api = applyRedisHelpers(pubClient, app);
|
const api = applyRedisHelpers(pubClient, app);
|
||||||
|
const ioHelpers = applyIOHelpers(app, api, ioRedis, logger);
|
||||||
|
|
||||||
// Legacy Socket Events
|
// Legacy Socket Events
|
||||||
require("./server/web-sockets/web-socket");
|
require("./server/web-sockets/web-socket");
|
||||||
|
|
||||||
applyMiddleware(app);
|
applyMiddleware(app);
|
||||||
applyRoutes(app);
|
applyRoutes(app);
|
||||||
redisSocketEvents(ioRedis, api);
|
redisSocketEvents(ioRedis, api, ioHelpers);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await server.listen(port);
|
await server.listen(port);
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ require("dotenv").config({
|
|||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
const { useremail, bodyshopid, operationName, variables, env, time, dbevent, user } = req.body;
|
const { useremail, bodyshopid, operationName, variables, env, time, dbevent, user } = req.body;
|
||||||
const { ioRedis } = req;
|
const {
|
||||||
|
ioRedis,
|
||||||
|
ioHelpers: { getBodyshopRoom }
|
||||||
|
} = req;
|
||||||
try {
|
try {
|
||||||
await client.request(queries.INSERT_IOEVENT, {
|
await client.request(queries.INSERT_IOEVENT, {
|
||||||
event: {
|
event: {
|
||||||
@@ -25,7 +28,7 @@ exports.default = async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ioRedis.to(bodyshopid).emit("bodyshop-message", {
|
ioRedis.to(getBodyshopRoom(bodyshopid)).emit("bodyshop-message", {
|
||||||
operationName,
|
operationName,
|
||||||
useremail
|
useremail
|
||||||
});
|
});
|
||||||
|
|||||||
32
server/job/job-updated.js
Normal file
32
server/job/job-updated.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const { isObject } = require("lodash");
|
||||||
|
|
||||||
|
const jobUpdated = async (req, res) => {
|
||||||
|
const { ioRedis, logger, ioHelpers } = req;
|
||||||
|
|
||||||
|
if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) {
|
||||||
|
logger.log("job-update-error", "ERROR", req.user?.email, null, {
|
||||||
|
message: `Malformed Job Update request sent from Hasura`,
|
||||||
|
body: req?.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
status: "error",
|
||||||
|
message: `Malformed Job Update request sent from Hasura`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("job-update", "INFO", req.user?.email, null, {
|
||||||
|
message: `Job updated event received from Hasura`,
|
||||||
|
jobid: req?.body?.event?.data?.new?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedJob = req.body.event.data.new;
|
||||||
|
const bodyshopID = updatedJob.shopid;
|
||||||
|
|
||||||
|
// Emit the job-updated event only to the room corresponding to the bodyshop
|
||||||
|
ioRedis.to(ioHelpers.getBodyshopRoom(bodyshopID)).emit("production-job-updated", updatedJob);
|
||||||
|
|
||||||
|
return res.json({ message: "Job updated and event emitted" });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = jobUpdated;
|
||||||
@@ -14,3 +14,4 @@ exports.costing = require("./job-costing").JobCosting;
|
|||||||
exports.costingmulti = require("./job-costing").JobCostingMulti;
|
exports.costingmulti = require("./job-costing").JobCostingMulti;
|
||||||
exports.statustransition = require("./job-status-transition").statustransition;
|
exports.statustransition = require("./job-status-transition").statustransition;
|
||||||
exports.lifecycle = require("./job-lifecycle");
|
exports.lifecycle = require("./job-lifecycle");
|
||||||
|
exports.jobUpdated = require("./job-updated");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const ppc = require("../ccc/partspricechange");
|
|||||||
const { partsScan } = require("../parts-scan/parts-scan");
|
const { partsScan } = require("../parts-scan/parts-scan");
|
||||||
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
|
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti } = require("../job/job");
|
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti, jobUpdated } = require("../job/job");
|
||||||
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
||||||
|
|
||||||
router.post("/totals", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals);
|
router.post("/totals", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals);
|
||||||
@@ -16,5 +16,6 @@ router.post("/lifecycle", validateFirebaseIdTokenMiddleware, withUserGraphQLClie
|
|||||||
router.post("/costingmulti", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti);
|
router.post("/costingmulti", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti);
|
||||||
router.post("/partsscan", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan);
|
router.post("/partsscan", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan);
|
||||||
router.post("/ppc", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, ppc.generatePpc);
|
router.post("/ppc", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, ppc.generatePpc);
|
||||||
|
router.post("/job-updated", eventAuthorizationMiddleware, jobUpdated);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
17
server/utils/ioHelpers.js
Normal file
17
server/utils/ioHelpers.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const applyIOHelpers = (app, api, io, logger) => {
|
||||||
|
const getBodyshopRoom = (bodyshopID) => `broadcast-room-${bodyshopID}`;
|
||||||
|
|
||||||
|
const ioHelpersAPI = {
|
||||||
|
getBodyshopRoom
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.ioHelpers = ioHelpersAPI;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
return ioHelpersAPI;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = applyIOHelpers;
|
||||||
@@ -11,32 +11,50 @@ function createLogEvent(socket, level, message) {
|
|||||||
logger.log("ioredis-log-event", level, socket.user.email, null, { wsmessage: message });
|
logger.log("ioredis-log-event", level, socket.user.email, null, { wsmessage: message });
|
||||||
}
|
}
|
||||||
|
|
||||||
const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRoom }) => {
|
const registerUpdateEvents = (socket) => {
|
||||||
|
socket.on("update-token", async (newToken) => {
|
||||||
|
try {
|
||||||
|
socket.user = await admin.auth().verifyIdToken(newToken);
|
||||||
|
createLogEvent(socket, "INFO", "Token updated successfully");
|
||||||
|
socket.emit("token-updated", { success: true });
|
||||||
|
} catch (error) {
|
||||||
|
createLogEvent(socket, "ERROR", `Token update failed: ${error.message}`);
|
||||||
|
socket.emit("token-updated", { success: false, error: error.message });
|
||||||
|
// Optionally disconnect the socket if token verification fails
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRoom }, { getBodyshopRoom }) => {
|
||||||
// Room management and broadcasting events
|
// Room management and broadcasting events
|
||||||
function registerRoomAndBroadcastEvents(socket) {
|
function registerRoomAndBroadcastEvents(socket) {
|
||||||
socket.on("join-bodyshop-room", async (bodyshopUUID) => {
|
socket.on("join-bodyshop-room", async (bodyshopUUID) => {
|
||||||
socket.join(bodyshopUUID);
|
const room = getBodyshopRoom(bodyshopUUID);
|
||||||
await addUserToRoom(bodyshopUUID, { uid: socket.user.uid, email: socket.user.email });
|
socket.join(room);
|
||||||
|
await addUserToRoom(room, { uid: socket.user.uid, email: socket.user.email });
|
||||||
createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${bodyshopUUID}`);
|
createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${bodyshopUUID}`);
|
||||||
|
|
||||||
// Notify all users in the room about the updated user list
|
// Notify all users in the room about the updated user list
|
||||||
const usersInRoom = await getUsersInRoom(bodyshopUUID);
|
const usersInRoom = await getUsersInRoom(bodyshopUUID);
|
||||||
io.to(bodyshopUUID).emit("room-users-updated", usersInRoom);
|
io.to(room).emit("room-users-updated", usersInRoom);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("leave-bodyshop-room", async (bodyshopUUID) => {
|
socket.on("leave-bodyshop-room", async (bodyshopUUID) => {
|
||||||
socket.leave(bodyshopUUID);
|
const room = getBodyshopRoom(bodyshopUUID);
|
||||||
createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${bodyshopUUID}`);
|
socket.leave(room);
|
||||||
|
createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${room}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("get-room-users", async (bodyshopUUID, callback) => {
|
socket.on("get-room-users", async (bodyshopUUID, callback) => {
|
||||||
const usersInRoom = await getUsersInRoom(bodyshopUUID);
|
const usersInRoom = await getUsersInRoom(getBodyshopRoom(bodyshopUUID));
|
||||||
callback(usersInRoom);
|
callback(usersInRoom);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("broadcast-to-bodyshop", async (bodyshopUUID, message) => {
|
socket.on("broadcast-to-bodyshop", async (bodyshopUUID, message) => {
|
||||||
io.to(bodyshopUUID).emit("bodyshop-message", message);
|
const room = getBodyshopRoom(bodyshopUUID);
|
||||||
createLogEvent(socket, "INFO", `Broadcast message to bodyshop ${bodyshopUUID}`);
|
io.to(room).emit("bodyshop-message", message);
|
||||||
|
createLogEvent(socket, "INFO", `Broadcast message to bodyshop ${room}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disconnect", async () => {
|
socket.on("disconnect", async () => {
|
||||||
@@ -45,12 +63,12 @@ const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRo
|
|||||||
// Get all rooms the socket is part of
|
// Get all rooms the socket is part of
|
||||||
const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id);
|
const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id);
|
||||||
|
|
||||||
for (const bodyshopUUID of rooms) {
|
for (const bodyshopRoom of rooms) {
|
||||||
await removeUserFromRoom(bodyshopUUID, { uid: socket.user.uid, email: socket.user.email });
|
await removeUserFromRoom(bodyshopRoom, { uid: socket.user.uid, email: socket.user.email });
|
||||||
|
|
||||||
// Notify all users in the room about the updated user list
|
// Notify all users in the room about the updated user list
|
||||||
const usersInRoom = await getUsersInRoom(bodyshopUUID);
|
const usersInRoom = await getUsersInRoom(bodyshopRoom);
|
||||||
io.to(bodyshopUUID).emit("room-users-updated", usersInRoom);
|
io.to(bodyshopRoom).emit("room-users-updated", usersInRoom);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -61,11 +79,7 @@ const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRo
|
|||||||
|
|
||||||
// Register room and broadcasting events
|
// Register room and broadcasting events
|
||||||
registerRoomAndBroadcastEvents(socket);
|
registerRoomAndBroadcastEvents(socket);
|
||||||
|
registerUpdateEvents(socket);
|
||||||
// Handle socket disconnection
|
|
||||||
socket.on("disconnect", async () => {
|
|
||||||
createLogEvent(socket, "DEBUG", `User disconnected.`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const authMiddleware = (socket, next) => {
|
const authMiddleware = (socket, next) => {
|
||||||
@@ -79,7 +93,7 @@ const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRo
|
|||||||
next();
|
next();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
next(new Error("Authentication error", JSON.stringify(error)));
|
next(new Error(`Authentication error: ${error.message}`));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
next(new Error("Authentication error - no authorization token."));
|
next(new Error("Authentication error - no authorization token."));
|
||||||
@@ -87,7 +101,6 @@ const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRo
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Uncaught connection error:::", error);
|
console.log("Uncaught connection error:::", error);
|
||||||
logger.log("websocket-connection-error", "error", null, null, {
|
logger.log("websocket-connection-error", "error", null, null, {
|
||||||
token: socket.handshake.auth.token,
|
|
||||||
...error
|
...error
|
||||||
});
|
});
|
||||||
next(new Error(`Authentication error ${error}`));
|
next(new Error(`Authentication error ${error}`));
|
||||||
|
|||||||
Reference in New Issue
Block a user