Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

* Added FDC3 Workbench support for `DesktopAgent` and `PrivateChannel` events, including automatic user channel updates, private channel event reporting, context-type-aware streaming, FDC3 2.0-2.1 compatibility, and copyable code examples. ([#1674](https://github.com/finos/FDC3/issues/1674))
Comment thread
akira-in-tech marked this conversation as resolved.
Outdated
* Added conformance coverage for `ChannelError.NoChannelFound`, `ChannelError.MalformedContext`, and `ChannelError.InvalidArguments`. ([#1779](https://github.com/finos/FDC3/issues/1779))
* Added conformance coverage verifying that Desktop Agent methods continue to work when destructured from the `fdc3` object. ([#1778](https://github.com/finos/FDC3/issues/1778))
* Added standalone Workbench examples for the FDC3 2.2 `fdc3.action`, `fdc3.fileAttachment`, `fdc3.message`, `fdc3.orderList`, `fdc3.tradeList`, and `fdc3.timeRange` context types. ([#1949](https://github.com/finos/FDC3/pull/1949))
Expand Down
1 change: 1 addition & 0 deletions toolbox/fdc3-workbench/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"start": "vite",
"preview": "vite preview",
"build": "tsc && vite build",
"test": "vitest run",
"lint": " eslint src/",
"lint:fix": "eslint --fix src/**/*.{ts,tsx} && prettier --write src/**/*.{ts,tsx}"
},
Expand Down
31 changes: 31 additions & 0 deletions toolbox/fdc3-workbench/src/components/ChannelField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,37 @@ export const ChannelField = observer(
</Link>
</Grid>
</Grid>
{isPrivateChannel && (
<Grid container sx={styles.secondMargin} alignItems="center">
<Grid item sx={styles.field}>
<Typography variant="h6" sx={styles.h6}>
Private channel events
</Typography>
<Typography variant="body2">
Received events are shown in the Workbench listeners panel.
</Typography>
</Grid>
<Grid item container sx={styles.controls} sm={5} justifyContent="flex-end">
<Tooltip title="Copy code example" aria-label="Copy code example">
<IconButton
size="small"
aria-label="Copy code example"
color="primary"
onClick={copyToClipboard(codeExamples.privateChannelEvents, 'privateChannelEvents')}
>
<FileCopyIcon />
</IconButton>
</Tooltip>
<Link
onClick={openApiDocsLink}
target="FDC3APIDocs"
href="https://fdc3.finos.org/docs/api/ref/PrivateChannel#addeventlistener"
>
<InfoOutlinedIcon />
</Link>
</Grid>
</Grid>
)}
<Button
variant="contained"
color="secondary"
Expand Down
40 changes: 40 additions & 0 deletions toolbox/fdc3-workbench/src/components/Channels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,46 @@ export const Channels = observer(

<div style={styles.border}></div>

<Grid item xs={12}>
<Typography variant="h5">User channel events</Typography>
</Grid>
<Grid container direction="row" justifyContent="space-between" sx={styles.controls}>
<Grid item sx={styles.dropDown}>
<Typography variant="body1">
{channelStore.isUserChannelChangedListenerActive
? 'Listening for userChannelChanged events'
: 'Event listener unavailable (requires FDC3 2.2+)'}
</Typography>
</Grid>
<Grid item>
<Grid container direction="row" justifyContent="flex-end" spacing={1}>
<Grid item sx={styles.controls}>
<Tooltip title="Copy code example" aria-label="Copy code example">
<IconButton
size="small"
aria-label="Copy code example"
color="primary"
onClick={copyToClipboard(codeExamples.userChannelChangedEvent, 'userChannelChanged')}
>
<FileCopyIcon />
</IconButton>
</Tooltip>
</Grid>
<Grid item sx={styles.controls}>
<Link
onClick={openApiDocsLink}
target="FDC3APIDocs"
href="https://fdc3.finos.org/docs/api/ref/DesktopAgent#addeventlistener"
>
<InfoOutlinedIcon />
</Link>
</Grid>
</Grid>
</Grid>
</Grid>

<div style={styles.border}></div>

<Grid item xs={12}>
<Typography variant="h5">Join user channels</Typography>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const IntentResolutionField = observer(
setPrivateChannel(true);
setChannelsList([{ id: channel.id, channel: channel }]);
privateChannelStore.addChannelListener(channel as PrivateChannel, 'all');
await privateChannelStore.listenForEvents(channel as PrivateChannel);
}
setResolutionResult(null);
} else if (result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const classes = {

export const PrivateChannelListeners = observer(() => {
const contextListeners: AccordionListItem[] = [];
const channelEvents: AccordionListItem[] = [];

privateChannelStore.channelListeners.forEach(({ id, channelId, type, lastReceivedContext, metaData }) => {
const receivedContextListenerValue = lastReceivedContext ? JSON.stringify(lastReceivedContext, undefined, 4) : '';
Expand Down Expand Up @@ -51,17 +52,30 @@ export const PrivateChannelListeners = observer(() => {
contextListeners.push({ id, textPrimary: `Channel Id: ${channelId}: ${type}`, afterEachElement: contextField });
});

privateChannelStore.privateChannelEvents.forEach(({ id, channelId, type, contextType }) => {
const eventDetails = type === 'disconnect' ? type : `${type}: ${contextType ?? 'all context types'}`;
channelEvents.push({ id, textPrimary: `Channel Id: ${channelId}: ${eventDetails}` });
});

const handleDeleteListener = (id: string) => {
privateChannelStore.removeContextListener(id);
};

return (
<AccordionList
title="Private Channels"
icon="Any context already in the channel will NOT be received automatically"
noItemsText="No Private Channel Listeners"
listItems={contextListeners}
onDelete={handleDeleteListener}
/>
<>
<AccordionList
title="Private Channels"
icon="Any context already in the channel will NOT be received automatically"
noItemsText="No Private Channel Listeners"
listItems={contextListeners}
onDelete={handleDeleteListener}
/>
<AccordionList
title="Private Channel Events"
icon="Shows context listener and disconnect events received from private channels"
noItemsText="No Private Channel Events"
listItems={channelEvents}
/>
</>
);
});
34 changes: 33 additions & 1 deletion toolbox/fdc3-workbench/src/fixtures/codeExamples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ let current = await fdc3.getCurrentChannel();
//leave the current channel\nawait fdc3.leaveCurrentChannel();
//the fdc3Listener will now cease receiving context`,

userChannelChangedEvent: `// FDC3 2.2+
const listener = await fdc3.addEventListener('userChannelChanged', event => {
console.log('Current user channel:', event.details.currentChannelId);
});

// Stop listening when the event is no longer needed
await listener.unsubscribe();`,

broadcast: `const instrument = {
type: 'fdc3.instrument',
id: {
Expand Down Expand Up @@ -78,6 +86,19 @@ const contactListener = appChannel.addContextListener('fdc3.contact', contact =>
//add context handling code here
});`,

privateChannelEvents: `// FDC3 2.2+
const added = await privateChannel.addEventListener('addContextListener', event => {
console.log('Listener added for:', event.details.contextType ?? 'all');
});

const removed = await privateChannel.addEventListener('unsubscribe', event => {
console.log('Listener removed for:', event.details.contextType ?? 'all');
});

const disconnected = await privateChannel.addEventListener('disconnect', () => {
console.log('The other participant disconnected');
});`,

intentListener: `const listener = fdc3.addIntentListener('StartChat', context => {
// start chat has been requested by another application
});`,
Expand All @@ -100,9 +121,20 @@ const listener = fdc3.addIntentListener('StartChat', context => {
return channel;
});`,

intentListenerWithPrivateChannel: `const listener = fdc3.addIntentListener('StartChat', context => {
intentListenerWithPrivateChannel: `const listener = fdc3.addIntentListener('StartChat', async context => {
// start chat has been requested by another application
const channel = await fdc3.createPrivateChannel();

await channel.addEventListener('addContextListener', event => {
console.log('Listener added for:', event.details.contextType ?? 'all');
});
await channel.addEventListener('unsubscribe', event => {
console.log('Listener removed for:', event.details.contextType ?? 'all');
});
await channel.addEventListener('disconnect', () => {
console.log('The other participant disconnected');
});

return channel;
});`,

Expand Down
7 changes: 7 additions & 0 deletions toolbox/fdc3-workbench/src/fixtures/logMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const getLogMessage = (name: logMessagesName, type: logMessagesType, valu
success: `Retrieved current channel [${value}]`,
error: `Failed to retrieve current channel`,
},
userChannelChanged: {
info: `User channel changed to [${value}]`,
error: `Failed to listen for user channel changes`,
},
joinUserChannel: {
success: `Joined the [${value}] channel`,
error: `Failed to join the [${value}] channel`,
Expand Down Expand Up @@ -112,6 +116,9 @@ export const getLogMessage = (name: logMessagesName, type: logMessagesType, valu
success: `${value}`,
error: `${value}`,
},
privateChannelEventListener: {
error: `Failed to listen for events on private channel [${value}]`,
},
};

return logMessages[name][type] ?? (value != '' ? `${value}` : `Undefined log message ${name}.${type}`);
Expand Down
82 changes: 82 additions & 0 deletions toolbox/fdc3-workbench/src/store/ChannelStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* SPDX-License-Identifier: Apache-2.0
* Copyright FINOS FDC3 contributors - see NOTICE file
*/
import { Channel, DesktopAgent, EventHandler, Listener } from '@finos/fdc3';
import { afterEach, describe, expect, it, vi } from 'vitest';
import systemLogStore from './SystemLogStore.js';
import { ChannelStore } from './ChannelStore.js';

const createListener = (): Listener => ({
unsubscribe: vi.fn(),
});

describe('ChannelStore event support', () => {
afterEach(() => {
systemLogStore.logList = [];
});

it('updates the current user channel when a 2.2 event is received', async () => {
const channels = [{ id: 'red' }, { id: 'green' }] as Channel[];
let eventHandler: EventHandler | undefined;
const agent = {
getUserChannels: vi.fn().mockResolvedValue(channels),
getCurrentChannel: vi.fn().mockResolvedValue(channels[0]),
getInfo: vi.fn().mockResolvedValue({ fdc3Version: '2.2' }),
addEventListener: vi.fn(async (_type: string, handler: EventHandler) => {
eventHandler = handler;
return createListener();
}),
} as unknown as DesktopAgent;
const store = new ChannelStore(async () => agent, false);

await store.getUserChannels();
await store.listenForUserChannelChanges();
eventHandler?.({
type: 'userChannelChanged',
details: { currentChannelId: 'green' },
});

expect(agent.addEventListener).toHaveBeenCalledWith('userChannelChanged', expect.any(Function));
expect(store.isUserChannelChangedListenerActive).toBe(true);
expect(store.currentUserChannel).toEqual(channels[1]);
expect(systemLogStore.logList.at(-1)?.message).toBe('User channel changed to [green]');
});

it('clears the current user channel when the event reports no channel', async () => {
const channel = { id: 'red' } as Channel;
let eventHandler: EventHandler | undefined;
const agent = {
getUserChannels: vi.fn().mockResolvedValue([channel]),
getCurrentChannel: vi.fn().mockResolvedValue(channel),
getInfo: vi.fn().mockResolvedValue({ fdc3Version: '3.0.0' }),
addEventListener: vi.fn(async (_type: string, handler: EventHandler) => {
eventHandler = handler;
return createListener();
}),
} as unknown as DesktopAgent;
const store = new ChannelStore(async () => agent, false);

await store.getUserChannels();
await store.listenForUserChannelChanges();
eventHandler?.({
type: 'userChannelChanged',
details: { currentChannelId: null },
});

expect(store.currentUserChannel).toBeNull();
});

it('does not register the new event API for FDC3 2.1', async () => {
const agent = {
getInfo: vi.fn().mockResolvedValue({ fdc3Version: '2.1' }),
addEventListener: vi.fn(),
} as unknown as DesktopAgent;
const store = new ChannelStore(async () => agent, false);

await store.listenForUserChannelChanges();

expect(agent.addEventListener).not.toHaveBeenCalled();
expect(store.isUserChannelChangedListenerActive).toBe(false);
});
});
Loading