Skip to content

Commit

Permalink
Merge branch 'master' into fake-bundle
Browse files Browse the repository at this point in the history
  • Loading branch information
apinkert committed Jul 12, 2023
2 parents 4800929 + 2c6f60a commit f29ebe7
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 10 deletions.
12 changes: 12 additions & 0 deletions config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
87 changes: 87 additions & 0 deletions docs/localWSDevelopment.md
Original file line number Diff line number Diff line change
@@ -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. <a name="bypass"></a>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"
}
```
15 changes: 11 additions & 4 deletions src/analytics/SegmentProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,20 @@ const getAPIKey = (env: SegmentEnvs = 'dev', module: SegmentModules, moduleAPIKe
}[env]?.[module] ||
KEY_FALLBACK[env];

let observer: MutationObserver | undefined;
const registerAnalyticsObserver = () => {
// never override the observer
if (observer) {
return;
}
/**
* We ignore hash changes
* Hashes only have frontend effect
*/
let oldHref = document.location.href.replace(/#.*$/, '');

const bodyList = document.body;
const observer = new MutationObserver((mutations) => {
observer = new MutationObserver((mutations) => {
mutations.forEach(() => {
const newLocation = document.location.href.replace(/#.*$/, '');
if (oldHref !== newLocation) {
Expand All @@ -99,7 +104,6 @@ const registerAnalyticsObserver = () => {
childList: true,
subtree: true,
});
return observer.disconnect;
};

const isInternal = (email = '') => /@(redhat\.com|.*ibm\.com)$/gi.test(email);
Expand Down Expand Up @@ -255,8 +259,11 @@ const SegmentProvider: React.FC<SegmentProviderProps> = ({ activeModule, childre
};

useEffect(() => {
const disconnect = registerAnalyticsObserver();
return () => disconnect();
registerAnalyticsObserver();
return () => {
observer?.disconnect();
observer = undefined;
};
}, []);

useEffect(() => {
Expand Down
5 changes: 2 additions & 3 deletions src/components/AppFilter/useAppFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type AppFilterBucket = {
links: NavItem[];
};

const previewBunldes = ['business-services', 'subscription-services'];
const previewBundles = ['business-services', 'subscriptions'];

export const requiredBundles = [
'application-services',
Expand All @@ -24,7 +24,7 @@ export const requiredBundles = [
'settings',
'iam',
'quay',
...(!isProd() ? previewBunldes : isBeta() ? previewBunldes : []),
...(!isProd() ? previewBundles : isBeta() ? previewBundles : []),
];

const bundlesOrder = [
Expand All @@ -39,7 +39,6 @@ const bundlesOrder = [
'iam',
'quay',
'business-services',
'subscription-services',
];

const isITLessEnv = ITLess();
Expand Down
4 changes: 2 additions & 2 deletions src/components/ChromeLink/ChromeLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export interface LinkWrapperProps extends RefreshLinkProps {
tabIndex?: number;
}

const LinkWrapper: React.FC<LinkWrapperProps> = memo(({ href, isBeta, onLinkClick, className, currAppId, appId, children, tabIndex }) => {
const LinkWrapper: React.FC<LinkWrapperProps> = memo(({ href = '', isBeta, onLinkClick, className, currAppId, appId, children, tabIndex }) => {
const linkRef = useRef<HTMLAnchorElement | null>(null);
const moduleRoutes = useSelector<ReduxState, RouteDefinition[]>(({ chrome: { moduleRoutes } }) => moduleRoutes);
const moduleEntry = useMemo(() => moduleRoutes.find((route) => href.includes(route.path)), [href, appId]);
const moduleEntry = useMemo(() => moduleRoutes.find((route) => href?.includes(route.path)), [href, appId]);
const preloadTimeout = useRef<NodeJS.Timeout>();
let actionId = href.split('/').slice(2).join('/');
if (actionId.includes('/')) {
Expand Down
4 changes: 4 additions & 0 deletions src/components/RootApp/ScalprumRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand All @@ -52,6 +53,9 @@ const ScalprumRoot = memo(
const store = useStore<ReduxState>();
const modulesConfig = useSelector(({ chrome: { modules } }: ReduxState) => modules);

// initialize WS event handling
useChromeServiceEvents();

const { setActiveTopic } = useHelpTopicManager(helpTopicsAPI);

function isStringArray(arr: EnableTopicsArgs): arr is string[] {
Expand Down
4 changes: 4 additions & 0 deletions src/components/Routes/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const redirects = [
path: '/hac',
to: '/hac/application-pipeline',
},
{
path: '/subscriptions',
to: '/subscriptions/overview',
},
];

export type RoutesProps = {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useBreadcrumbsLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const useBreadcrumbsLinks = () => {
appFragments.pop();
// Match first parent route. Routes are taken directly from router definitions.
const fallbackMatch = matchRoutes(wildCardRoutes, activeItem.href) || [];
const fallbackMatchFragments = fallbackMatch?.[0].pathnameBase.split('/');
const fallbackMatchFragments = fallbackMatch?.[0]?.pathnameBase.split('/');
const groupFragments: Required<NavItem, 'href'>[] = navItems.map((item, index) => ({
...item,
/**
Expand Down
89 changes: 89 additions & 0 deletions src/hooks/useChromeServiceEvents.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Payload = Payload> {
type: EventTypes;
data: T;
}

function isGenericEvent(event: unknown): event is GenericEvent {
return typeof event === 'object' && event !== null && ALL_TYPES.includes((event as Record<string, never>).type);
}

const useChromeServiceEvents = () => {
const connection = useRef<WebSocket | undefined>();
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;
2 changes: 2 additions & 0 deletions src/jwt/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`);
Expand Down

0 comments on commit f29ebe7

Please sign in to comment.