diff --git a/config/webpack.config.js b/config/webpack.config.js index 4aa141f44..e8a98d67f 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -152,6 +152,18 @@ const commonConfig = ({ dev }) => { proxyVerbose: true, isChrome: true, routes: { + ...(process.env.CHROME_SERVICE && { + // web sockets + '/wss/chrome-service/': { + target: `ws://localhost:${process.env.CHROME_SERVICE}`, + // To upgrade the connection + ws: true, + }, + // REST API + '/api/chrome-service/v1/': { + host: `http://localhost:${process.env.CHROME_SERVICE}`, + }, + }), ...(process.env.CONFIG_PORT && { '/beta/config': { host: `http://localhost:${process.env.CONFIG_PORT}`, diff --git a/docs/localWSDevelopment.md b/docs/localWSDevelopment.md new file mode 100644 index 000000000..004d61c6c --- /dev/null +++ b/docs/localWSDevelopment.md @@ -0,0 +1,87 @@ +# Local WS development + +## Prerequisites + +1. Have a go language setup. You can follow the [gmv guide](https://github.com/moovweb/gvm#installing). +2. Have a podman installed. [Getting started guide](https://podman.io/get-started) +3. Have the [chrome-service-backend](https://github.com/RedHatInsights/chrome-service-backend) checkout locally. +4. Make sure you terminal supports the [Makefile](https://makefiletutorial.com/) utility. + +## Setting up the development environment + +### Chrome service backend + +The chrome service backend is the bridge between kafka and the browser client. It exposes a WS endpoint that allows the browser to connect to the service. + +> **__Note__** these steps start only the minimal required infrastructure. + +To enable it for local development with chrome UI follow these steps: + +1. Make sure you are on the `main` branch or have created new branch fro the latest `main`. +2. If the WS feature is hidden behind a feature flag, and you don't want to go through the FF setup, [bypass this condition](https://github.com/RedHatInsights/chrome-service-backend/blob/main/main.go#L61) by adding `true || ..` to the condition in your local repository. +3. Start the kafka container by running `make kafka`. +4. Start the chrome service by running `go run .` in the repo root. +5. Run a `go run cmd/kafka/testMessage.go` to test the connection. You should see a log in the terminal from where the go server was started. + +#### The `make kafka` command failed. + +It is possible that you have services running on your machine that occupy either the kafka or postgres ports. To change the default values, open the `local/kafka-compose.yaml` and change the postgres or kafka (or both) port bindings: + +```diff +diff --git a/local/kafka-compose.yaml b/local/kafka-compose.yaml +index f8f3451..60fc9cd 100644 +--- a/local/kafka-compose.yaml ++++ b/local/kafka-compose.yaml +@@ -8,7 +8,7 @@ services: + - POSTGRES_USER=chrome + - POSTGRES_PASSWORD=chrome + ports: +- - "5432:5432" ++ - "5555:5432" + volumes: + - db:/var/lib/postgresql/data + +@@ -17,7 +17,7 @@ services: + hostname: zoo1 + container_name: zoo1 + ports: +- - "2181:2181" ++ - "8888:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_SERVER_ID: 1 + +``` + +#### The `go run cmd/kafka/testMessage.go` + +The command will not work if the main server is not running, or the ws consumer did not start. Ensure you either have feature flags setup or you have bypassed the condition listed in the [step 2](#bypass). + + +### Chrome frontend + +1. ensure you are on the latest version for chrome `master` branch. The support for WS client was added in [#2525](https://github.com/RedHatInsights/insights-chrome/pull/2525). +2. Once your chrome service backend is running, start the chrome dev server with the chrome service config using this command: `CHROME_SERVICE=8000 yarn dev`. +3. The WS client connection is hidden behind a `platform.chrome.notifications-drawer` feature flag. If its not available in your current environment (stage or prod or EE), bypass the feature flag condition in the `src/hooks/useChromeServiceEvents.ts` file. +4. OPen the browser, open the browser console, emit a custom message from the chrome service terminal using `go run cmd/kafka/testMessage.go`. +5. In the network tab of your browser console, filter only to show `ws` communication, click on related ws connection (there will be a couple for webpack dev server, ignore these) and observer the messages being pushed down to the browser. + +#### Sample WS message payload + +```js +{ + // the actual payload consumed by chrome + data: { + description: "Some longer description", + title: "New notification" + }, + // cloud events sub protocol metadata + datacontenttype: "application/json", + id: "test-message", + source: "https://whatever.service.com", + specversion: "1.0.2", + time: "2023-05-23T11:54:03.879689005+02:00", + // a type field used to identify message purpose + type: "notifications.drawer" +} +``` diff --git a/src/components/RootApp/ScalprumRoot.tsx b/src/components/RootApp/ScalprumRoot.tsx index 6e6da13b5..a80369e05 100644 --- a/src/components/RootApp/ScalprumRoot.tsx +++ b/src/components/RootApp/ScalprumRoot.tsx @@ -28,6 +28,7 @@ import useBundleVisitDetection from '../../hooks/useBundleVisitDetection'; import chromeApiWrapper from './chromeApiWrapper'; import { ITLess } from '../../utils/common'; import InternalChromeContext from '../../utils/internalChromeContext'; +import useChromeServiceEvents from '../../hooks/useChromeServiceEvents'; const ProductSelection = lazy(() => import('../Stratosphere/ProductSelection')); @@ -52,6 +53,9 @@ const ScalprumRoot = memo( const store = useStore(); const modulesConfig = useSelector(({ chrome: { modules } }: ReduxState) => modules); + // initialize WS event handling + useChromeServiceEvents(); + const { setActiveTopic } = useHelpTopicManager(helpTopicsAPI); function isStringArray(arr: EnableTopicsArgs): arr is string[] { diff --git a/src/hooks/useChromeServiceEvents.ts b/src/hooks/useChromeServiceEvents.ts new file mode 100644 index 000000000..8beb35399 --- /dev/null +++ b/src/hooks/useChromeServiceEvents.ts @@ -0,0 +1,89 @@ +import { useEffect, useMemo, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { useFlag } from '@unleash/proxy-client-react'; + +import { getEncodedToken, setCookie } from '../jwt/jwt'; + +const NOTIFICATION_DRAWER = 'notifications.drawer'; +const SAMPLE_EVENT = 'sample.type'; + +const ALL_TYPES = [NOTIFICATION_DRAWER, SAMPLE_EVENT] as const; +type EventTypes = typeof ALL_TYPES[number]; + +type SamplePayload = { + foo: string; +}; + +type NotificationPayload = { + title: string; + description: string; +}; + +type Payload = NotificationPayload | SamplePayload; +interface GenericEvent { + type: EventTypes; + data: T; +} + +function isGenericEvent(event: unknown): event is GenericEvent { + return typeof event === 'object' && event !== null && ALL_TYPES.includes((event as Record).type); +} + +const useChromeServiceEvents = () => { + const connection = useRef(); + const dispatch = useDispatch(); + const isNotificationsEnabled = useFlag('platform.chrome.notifications-drawer'); + + const handlerMap: { [key in EventTypes]: (payload: Payload) => void } = useMemo( + () => ({ + [NOTIFICATION_DRAWER]: (data: Payload) => dispatch({ type: 'foo', payload: data }), + [SAMPLE_EVENT]: (data: Payload) => console.log('Received sample payload', data), + }), + [] + ); + + function handleEvent(type: EventTypes, data: Payload): void { + handlerMap[type](data); + } + + const createConnection = async () => { + const token = getEncodedToken(); + if (token) { + const socketUrl = `${document.location.origin.replace(/^.+:\/\//, 'wss://')}/wss/chrome-service/v1/ws`; + // ensure the cookie exists before we try to establish connection + await setCookie(token); + + // create WS URL from current origin + // ensure to use the cloud events sub protocol + const socket = new WebSocket(socketUrl, 'cloudevents.json'); + connection.current = socket; + + socket.onmessage = (event) => { + const { data } = event; + try { + const payload = JSON.parse(data); + if (isGenericEvent(payload)) { + handleEvent(payload.type, payload.data); + } else { + throw new Error(`Unable to handle event type: ${event.type}. The payload does not have required shape! ${event}`); + } + } catch (error) { + console.error('Handler failed when processing WS payload: ', data, error); + } + }; + } + }; + + useEffect(() => { + try { + // create only one connection and only feature is enabled + if (isNotificationsEnabled && !connection.current) { + createConnection(); + } + } catch (error) { + console.error('Unable to establish WS connection'); + } + }, [isNotificationsEnabled]); +}; + +export default useChromeServiceEvents; diff --git a/src/jwt/jwt.ts b/src/jwt/jwt.ts index 825f2f70a..3a43eb3d9 100644 --- a/src/jwt/jwt.ts +++ b/src/jwt/jwt.ts @@ -442,6 +442,8 @@ export async function setCookie(token?: string) { if (cookieName) { setCookieWrapper(`${cookieName}=${tok};` + `path=/wss;` + `secure=true;` + `expires=${getCookieExpires(tokExpires)}`); setCookieWrapper(`${cookieName}=${tok};` + `path=/ws;` + `secure=true;` + `expires=${getCookieExpires(tokExpires)}`); + setCookieWrapper(`${cookieName}=${tok};` + `path=wss://;` + `secure=true;` + `expires=${getCookieExpires(tokExpires)}`); + setCookieWrapper(`${cookieName}=${tok};` + `path=ws://;` + `secure=true;` + `expires=${getCookieExpires(tokExpires)}`); setCookieWrapper(`${cookieName}=${tok};` + `path=/api/tasks/v1;` + `secure=true;` + `expires=${getCookieExpires(tokExpires)}`); setCookieWrapper(`${cookieName}=${tok};` + `path=/api/automation-hub;` + `secure=true;` + `expires=${getCookieExpires(decodeToken(tok).exp)}`); setCookieWrapper(`${cookieName}=${tok};` + `path=/api/remediations/v1;` + `secure=true;` + `expires=${getCookieExpires(tokExpires)}`);