Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -36,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* Added labels and tooltips to dropdowns in the FDC3 for Web reference implementation demo. ([#193](https://github.com/finos/FDC3/pull/1932))

* Added `fdc3.close()` API call allowing an app to request that its own window or frame be closed, with `closeRequest`/`closeResponse` DACP messages and `CloseError` enumeration ([#1918](https://github.com/finos/FDC3/pull/1918))
* 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))

* Added an optional `newInstance` parameter to the `raiseIntent` and `raiseIntentForContext` API calls, allowing an app to explicitly request that a **new instance** of the target application be launched (`newInstance: true`) or that an **existing instance** be used and a new one never launched (`newInstance: false`, which rejects with `ResolveError.TargetInstanceUnavailable` if no running instance is available). Omitting the parameter (or passing `null` or `undefined`) preserves the Desktop Agent's default resolution behavior. The parameter is carried on the `raiseIntentRequest` / `raiseIntentForContextRequest` DACP payloads, implemented in the agent proxy and reference web implementation, and covered by unit and conformance tests (`RaiseIntentNewInstanceForced`, `RaiseIntentExistingInstanceRequired`, `RaiseIntentFailExistingInstanceRequired`). ([#1940](https://github.com/finos/FDC3/issues/1940))

Expand Down
2 changes: 2 additions & 0 deletions toolbox/fdc3-workbench/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"start": "vite",
"preview": "vite preview",
"build": "tsc && vite build",
"pretest": "npm run build --workspace=@finos/fdc3",
"test": "vitest run",
"lint": " eslint src/",
"lint:fix": "eslint --fix src/**/*.{ts,tsx} && prettier --write src/**/*.{ts,tsx}"
},
Expand Down
33 changes: 32 additions & 1 deletion toolbox/fdc3-workbench/src/components/ChannelField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export const ChannelField = observer(

const handleRemoveOrDisconnect = (channel: Fdc3ChannelRecord) => {
if (isPrivateChannel) {
privateChannelStore.disconnect(channel.channel as PrivateChannel);
void privateChannelStore.disconnect(channel.channel as PrivateChannel);
} else {
appChannelStore.remove(channel.channel);
}
Expand Down 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