Skip to content

Commit

Permalink
Added platform override example and fixes (#745)
Browse files Browse the repository at this point in the history
* Added platform override example and fixes

Added an example platform override module that can update saved urls if the app definition changes or show an access denied page if the user no longer has access to the app.

Updated the launch snapshot logic so that view ids are consistently returned.

Updated page logic to ensure that page view ids with guids are regenerated to prevent clashes (where a view is moved from a page to another window and then the page is relaunched).

Added docs and updated the changelog.

* Removed the name as this snapshot is shared with other samples.
  • Loading branch information
johnman authored Sep 9, 2024
1 parent 61243a3 commit 2afe54c
Show file tree
Hide file tree
Showing 13 changed files with 562 additions and 49 deletions.
5 changes: 5 additions & 0 deletions how-to/workspace-platform-starter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
- Updated Snap to 0.4.1
- Updated SnapProvider configuration so that it can support all snap server options and not just the show debug window setting. The showDebugWindow setting has been removed but there is backwards compatibility if it is specified in JSON. Please see [how to configure snap](./docs/how-to-configure-snap.md) for the latest configuration.
- You can now specify a path to a snap exe instead of relying on the path of an app asset if you do have it in a specific installed in a specific location.
- Updated apply snapshot logic so that it always returns all the view ids that were created.
- Fixed bug where you could have a page with apps that is saved, the apps are moved and the page is closed without saving. Launching the page would move the apps from the other windows as the ids were not unique. The name of views that represent apps now behave like platform views where the guid (if it exists) is updated. This behavior now reflects the behavior of duplicate page.
- Snapshots that contain apps can now list the name as appId/guid (similar to how they are launched in a layout). If a guid is detected the snapshot entries are updated when applied (to avoid clashes if you launch multiple instances).
- Fixed an issue where an empty object was being returned (instead of undefined) when getPage was called and it didn't exist.
- Added an example of a platform override module that validates app urls and whether the app is still supported in workspaces, snapshots and pages. This is just an example (server side is a better place) to show how the behavior could work. [application-url-and-access-validator](./client/src/modules/platform-override/application-url-and-access-validator/)

## v19.0.0

Expand Down
66 changes: 36 additions & 30 deletions how-to/workspace-platform-starter/client/src/framework/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ import { launchConnectedApp } from "./connections";
import * as endpointProvider from "./endpoint";
import { createLogger } from "./logger-provider";
import { MANIFEST_TYPES } from "./manifest-types";
import {
bringViewToFront,
bringWindowToFront,
doesViewExist,
doesWindowExist,
findViewNames
} from "./platform/browser";
import { bringViewToFront, bringWindowToFront, doesViewExist, doesWindowExist } from "./platform/browser";
import type {
NativeLaunchOptions,
PlatformApp,
Expand Down Expand Up @@ -794,33 +788,20 @@ async function launchSnapshot(snapshotApp: PlatformApp): Promise<PlatformAppIden
const viewIds: PlatformAppIdentifier[] = [];

for (const currentWindow of windows) {
let getViewIdsForLayout = findViewNames(currentWindow.layout);
if (Array.isArray(currentWindow.workspacePlatform?.pages)) {
for (const page of currentWindow.workspacePlatform.pages) {
getViewIdsForLayout = getViewIdsForLayout.concat(findViewNames(page.layout));
for (let i = 0; i < currentWindow.workspacePlatform.pages.length; i++) {
let page = currentWindow.workspacePlatform.pages[i];
page = updateInstanceIds(page.layout);
currentWindow.workspacePlatform.pages[i] = page;
}
}
getViewIdsForLayout = [...new Set(getViewIdsForLayout)];

if (getViewIdsForLayout.length === 0) {
const uuid = randomUUID();
const name = `${snapshotApp.appId}/${uuid}`;
currentWindow.name = name;
windowsToCreate.push(currentWindow);
windowsToGather.push(name);
} else {
// we have views. Grab the first one to validate existence.
const viewId = getViewIdsForLayout[0];

for (const entry of getViewIdsForLayout) {
viewIds.push({ name: entry, uuid: fin.me.identity.uuid, appId: snapshotApp.appId });
}

// these views should be readonly and cannot be pulled out of the page or closed
if (!(await doesViewExist({ name: viewId, uuid: fin.me.identity.uuid }))) {
windowsToCreate.push(currentWindow);
}
currentWindow.layout = updateInstanceIds(currentWindow.layout);
}
const uuid = randomUUID();
const name = currentWindow.name ?? `${snapshotApp.appId}/${uuid}`;
currentWindow.name = name;
windowsToCreate.push(currentWindow);
windowsToGather.push(name);
}

manifest.windows = windowsToCreate;
Expand Down Expand Up @@ -1139,3 +1120,28 @@ export function isValidUrl(
}
return true;
}

/**
* Takes a layout and walks through all the nodes and applies logic to nodes that have
* a url and a name that matches a pattern. Updates the name to make it unique (if applicable)
* while retaining information related to an application's identity if present.
* @param layout The layout to duplicate
* @returns The duplicated layout.
*/
function updateInstanceIds<T>(layout: T): T {
return JSON.parse(
JSON.stringify(layout, (_, nestedValue) => {
// check to ensure that we have a name field and that we also have a url field in this object (in case name was added to a random part of the layout)
if (isStringValue(nestedValue?.name) && !isEmpty(nestedValue.url)) {
if (/\/[\d,a-z-]{36}$/.test(nestedValue.name)) {
nestedValue.name = nestedValue.name.replace(/([\d,a-z-]{36}$)/, randomUUID());
}
// case: internal-generated-view-<uuid>
if (/-[\d,a-z-]{36}$/.test(nestedValue.name)) {
nestedValue.name = nestedValue.name.replace(/(-[\d,a-z-]{36}$)/, randomUUID());
}
}
return nestedValue as unknown;
})
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Application Url and Access Validator

## What is this?

This is an example and this type of logic is best placed on the server (to reduce load on the client) but we are giving a client example to show you a simple example and to demonstrate how platform overrides can be used for very specific use cases.
This module is off by default but you can turn it on to see quick examples.

This example is related to the following scenario:

- I (Platform Owner) have a list of apps - those apps might be a view, window or a snapshot that is a collection of apps
- I allow my users to save Pages, Workspaces or share a Page or Workspace

What happens when:

- The saved page or workspace is loaded but the url for a particular app e.g. companyx.com/v1/app.html has been updated to companyx.com/v2/app.html?
- The user (or the user that has received the shared page/workspace) no longer has access to an app (or never did in the case of sharing a page/workspace)?
- There is one app (e.g. Salesforce) which might be an app in my directory with a starting point url but the app can be launched with different urls (e.g. pointing to a company or user) and I don't want the url to change in that scenario when the page or workspace is loaded.

## What does it do?

It provides an override for applySnapshot (used when loading snapshots and workspaces) and getPage (used for fetching a page to add it to a window).

- Each override function calls a common function that goes through the data finding view or window entries that are listed as apps.
- If it is an app then it determines whether or not the app allows the apps url to be updatable (then it leaves the last url saved) or if the app url is overridden (then it takes the overridden url rather than getting it from a manifest or inline manifest).
- If it is an app that is not present in the app directory then it replaces the url with the defined (through module settings) no access page and passes the appId using customData (so the target page can decide what to do with that information).

## How is it configured?

This example module is defined as a platform override module in a manifest or settings service:

```json
{
"id": "application-url-and-access-validator",
"icon": "http://localhost:8080/favicon.ico",
"title": "Application Url and Access Validator",
"description": "This is an example platform override module that shows how you could validate a saved page, workspace or an application snapshot (if it combines apps) to ensure that it is using the latest url for the applications used and that the user still has access to that app.",
"enabled": false,
"url": "http://localhost:8080/js/modules/platform-override/application-url-and-access-validator.bundle.js",
"data": {
"deniedAccessUrl": "http://localhost:8080/common/views/platform/no-access/no-access.html"
}
},
```

This is defined in the modules section of the platformProvider settings.

Order is important when it comes to platform and interop overrides. The request will hit the first entry in the array and every time super.x is called it will go to the next entry in the array until it hits the default implementation.

For this example we want to intercept the getPage request **before** the default workspace platform starter implementation. This lets our module get the page from the workspace platform starter platform override module (which might call super.getPage to call the default implementation or it can return the page if it has been configured to store pages in a backend server). If this module was placed after the workspace platform starter module in the array and it was configured to save and get pages from a server then our implementation of getPage would never be called.

## How can I test this?

- You would enable this module in the manifest.fin.json file in the public folder.
- You would launch the platform using npm run client
- You would then launch the call app and some other apps through home (or store).
- You would use the browser to save a page e.g. 'page with call' and a workspace e.g. 'call wks'.

### Access Denied

- Navigate to the place where the call app is defined: [public/common/apps-contact.json](../../../../../public/common/apps-contact.json)
- You will find the call-app defined in the top of the list. Change the id of call-app to call-app-2 and save the file (we cache the app directory for 10 seconds by default so it should pick up the new changes).
- Close the page 'page with call' that has the call app.
- Save a second workspace e.g. 'no call wks' to make it the current workspace.
- Now launch the page 'page with call' -> You should see the page launch with the access denied view
- Switch workspaces to load 'call wks' -> You should see the workspace load and the access denied view show where you had saved it in the workspace.

### App Url Changed

- Switch back to the 'no call wks' so that you have a layout that doesn't have any apps that have had changes.
- Go back to the call app entry in [public/common/apps-contact.json](../../../../../public/common/apps-contact.json) and rename the appId so that it goes back to call-app.
- Update the manifest path for the call app from <http://localhost:8080/common/views/contact/call-app.view.fin.json> to <http://localhost:8080/common/views/contact/call-app-v2.view.fin.json>.
- Navigate to the folder containing the call-app.view.fin.json file ([public/common/views/contact](../../../../../public/common/views/contact/)) and duplicate the file calling it call-app-v2.view.fin.json
- Update the url inside the new manifest call-app-v2.view.fin.json from <http://localhost:8080/common/views/contact/call-app/index.html> to <https://www.google.com> (just to make it easy to see the change).
- Now launch the page 'page with call' -> You should see the page launch with the google site shown
- Switch workspaces to load 'call wks' -> You should see the workspace load and the google page should show where you had saved the call app in the workspace.

If inline-view or inline-window was used then the logic will fetch the url from the inline manifest as it doesn't need to do a fetch to get an external json file.

## Where can I find out more about Platform Overrides

[How To Customize Your Platform Override](../../../../../docs/how-to-customize-your-platform-override.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ModuleImplementation, ModuleTypes } from "workspace-platform-starter/shapes/module-shapes";
import { ApplicationUrlAndAccessValidator } from "./platform-override";

/**
* Define the entry points for the module.
*/
export const entryPoints: { [type in ModuleTypes]?: ModuleImplementation } = {
platformOverride: new ApplicationUrlAndAccessValidator()
};
Loading

0 comments on commit 2afe54c

Please sign in to comment.