mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
## Summary This PR makes URL serialization logic available to Node.js contexts (like Cypress tests) and replaces a hard-coded 4812-character base64 URL in tests with programmatically generated state. This builds on the shared utilities refactoring from #8246. ### Changes #### 1. Extract URL Serialization to Shared Module **Problem:** URL serialization code depended on GoldenLayout's browser-only ConfigMinifier, preventing Cypress spec files from importing it (they load in Node.js before running in browser). **Solution:** Created `shared/url-serialization.ts` with a Node-compatible ConfigMinifier reimplementation. **Technical Details:** - Reimplemented GoldenLayout's ConfigMinifier without browser dependencies - Moved serialization functions (`serialiseState`, `deserialiseState`, `risonify`, `unrisonify`) to shared module - Moved minification functions (`minifyConfig`, `unminifyConfig`) to shared module - Updated `static/url.ts` to use shared module instead of GoldenLayout - Added comprehensive test coverage in `test/url-serialization.ts` **Files:** - **New:** `shared/url-serialization.ts` (~279 lines) - **Modified:** `static/url.ts` (removed ~30 lines, eliminated GoldenLayout dependency) - **New:** `test/url-serialization.ts` (~96 lines) #### 2. Replace Hard-coded Cypress URL with Programmatic State **Before:** A hard-coded 4812-character base64 URL containing state for all panes ```typescript cy.visit('http://localhost:10240/#z:OYLghAFBqd5TB8IAsQGMD2ATApgUWwEsAXTAJwBoiQIAzIgG...'); ``` **After:** Programmatically generated state using `buildKnownGoodState()` function ```typescript const state = buildKnownGoodState(); const hash = serialiseState(state); cy.visit(`http://localhost:10240/#${hash}`, {...}); ``` **Benefits:** - Human-readable, maintainable test state - Programmatic generation from `PANE_DATA_MAP` keys - Layout optimized with 8 panes per row - Produces identical compressed URL format - Much easier to add/modify panes in the future #### 3. PANE_DATA_MAP Consistency Improvements Updated `PANE_DATA_MAP` to use component names exactly as registered with GoldenLayout: **Key renames:** - `preprocessor` → `pp` - `llvmir` → `ir` - `pipeline` → `llvmOptPipelineView` - `mir` → `rustmir` - `hir` → `rusthir` - `macro` → `rustmacroexp` - `core` → `haskellCore` - `stg` → `haskellStg` - `cmm` → `haskellCmm` - `dump` → `gccdump` - `tree` → `gnatdebugtree` - `debug` → `gnatdebug` **Added panes:** `codeEditor`, `compiler`, `conformance`, `output` (were missing from map) **Re-enabled tests:** - `yul` pane test (was commented out, now fixed) - `clojuremacroexp` pane test (was commented out, now fixed) - `cfg` pane test (had TODO, now removed) **Why this matters:** The `buildKnownGoodState()` function uses `Object.keys(PANE_DATA_MAP)` as the `componentName` property, so keys must match the actual registered component names for GoldenLayout to find them. ## Test Plan - [x] All Cypress tests pass (confirmed by @mattgodbolt) - [x] TypeScript compilation passes (`npm run ts-check`) - [x] Linting passes (`npm run lint`) - [x] URL serialization tests pass (3/3 tests) - [x] Pre-commit hooks pass - [x] Related vitest tests pass ## Dependencies - Builds on #8246 (shared utilities refactoring - already merged) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
530 lines
20 KiB
TypeScript
530 lines
20 KiB
TypeScript
// Copyright (c) 2021, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import {Modal, Tooltip} from 'bootstrap';
|
|
import ClipboardJS from 'clipboard';
|
|
import GoldenLayout from 'golden-layout';
|
|
import $ from 'jquery';
|
|
import _ from 'underscore';
|
|
import {unwrap} from '../shared/assert.js';
|
|
import {serialiseState} from '../shared/url-serialization.js';
|
|
import * as BootstrapUtils from './bootstrap-utils.js';
|
|
import {sessionThenLocalStorage} from './local.js';
|
|
import {options} from './options.js';
|
|
import {SentryCapture} from './sentry.js';
|
|
import {Settings, SiteSettings} from './settings.js';
|
|
|
|
import ClickEvent = JQuery.ClickEvent;
|
|
|
|
import cloneDeep from 'lodash.clonedeep';
|
|
|
|
enum LinkType {
|
|
Short = 0,
|
|
Full = 1,
|
|
Embed = 2,
|
|
}
|
|
|
|
const shareServices = {
|
|
twitter: {
|
|
embedValid: false,
|
|
logoClass: 'fab fa-twitter',
|
|
cssClass: 'share-twitter',
|
|
getLink: (title: string, url: string) => {
|
|
return (
|
|
'https://twitter.com/intent/tweet' +
|
|
`?text=${encodeURIComponent(title)}` +
|
|
`&url=${encodeURIComponent(url)}` +
|
|
'&via=CompileExplore'
|
|
);
|
|
},
|
|
text: 'Tweet',
|
|
},
|
|
bluesky: {
|
|
embedValid: false,
|
|
logoClass: 'fab fa-bluesky',
|
|
cssClass: 'share-bluesky',
|
|
getLink: (title: string, url: string) => {
|
|
const text = `${title} ${url} via @compiler-explorer.com`;
|
|
return `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`;
|
|
},
|
|
text: 'Share on Bluesky',
|
|
},
|
|
reddit: {
|
|
embedValid: false,
|
|
logoClass: 'fab fa-reddit',
|
|
cssClass: 'share-reddit',
|
|
getLink: (title: string, url: string) => {
|
|
return (
|
|
'http://www.reddit.com/submit' +
|
|
`?url=${encodeURIComponent(url)}` +
|
|
`&title=${encodeURIComponent(title)}`
|
|
);
|
|
},
|
|
text: 'Share on Reddit',
|
|
},
|
|
};
|
|
|
|
// Base class that handles state tracking and embedded link updates
|
|
export class SharingBase {
|
|
protected layout: GoldenLayout;
|
|
protected lastState: string | null = null;
|
|
|
|
constructor(layout: GoldenLayout) {
|
|
this.layout = layout;
|
|
this.initCallbacks();
|
|
}
|
|
|
|
protected initCallbacks(): void {
|
|
this.layout.on('stateChanged', this.onStateChanged.bind(this));
|
|
}
|
|
|
|
protected onStateChanged(): void {
|
|
const config = SharingBase.filterComponentState(this.layout.toConfig());
|
|
this.ensureUrlIsNotOutdated(config);
|
|
|
|
// Update embedded links if present (works in both modes)
|
|
if (options.embedded) {
|
|
const strippedToLast = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
|
|
$('a.link').prop('href', strippedToLast + '#' + serialiseState(config));
|
|
}
|
|
}
|
|
|
|
protected ensureUrlIsNotOutdated(config: any): void {
|
|
const stringifiedConfig = JSON.stringify(config);
|
|
if (stringifiedConfig !== this.lastState) {
|
|
if (this.lastState != null && window.location.pathname !== window.httpRoot) {
|
|
window.history.replaceState(null, '', window.httpRoot);
|
|
}
|
|
this.lastState = stringifiedConfig;
|
|
}
|
|
}
|
|
|
|
public static filterComponentState(config: any): any {
|
|
function filterComponentStateImpl(component: any) {
|
|
if (component.content) {
|
|
for (let i = 0; i < component.content.length; i++) {
|
|
filterComponentStateImpl(component.content[i]);
|
|
}
|
|
}
|
|
|
|
if (component.componentState) {
|
|
delete component.componentState.selection;
|
|
}
|
|
}
|
|
|
|
config = cloneDeep(config);
|
|
filterComponentStateImpl(config);
|
|
return config;
|
|
}
|
|
}
|
|
|
|
export class Sharing extends SharingBase {
|
|
private readonly share: JQuery;
|
|
private readonly shareTooltipTarget: JQuery;
|
|
private readonly shareShort: JQuery;
|
|
private readonly shareFull: JQuery;
|
|
private readonly shareEmbed: JQuery;
|
|
|
|
private settings: SiteSettings;
|
|
|
|
private clippyButton: ClipboardJS | null;
|
|
private readonly shareLinkDialog: HTMLElement;
|
|
|
|
constructor(layout: GoldenLayout) {
|
|
super(layout);
|
|
this.shareLinkDialog = unwrap(document.getElementById('sharelinkdialog'), 'Share modal element not found');
|
|
|
|
this.share = $('#share');
|
|
this.shareTooltipTarget = $('#share-tooltip-target');
|
|
this.shareShort = $('#shareShort');
|
|
this.shareFull = $('#shareFull');
|
|
this.shareEmbed = $('#shareEmbed');
|
|
|
|
[this.shareShort, this.shareFull, this.shareEmbed].forEach(el => {
|
|
el.on('click', e => BootstrapUtils.showModal(this.shareLinkDialog, e.currentTarget));
|
|
});
|
|
this.settings = Settings.getStoredSettings();
|
|
|
|
this.clippyButton = null;
|
|
|
|
this.initButtons();
|
|
this.initFullCallbacks();
|
|
}
|
|
|
|
private initFullCallbacks(): void {
|
|
this.layout.eventHub.on('displaySharingPopover', () => {
|
|
this.openShareModalForType(LinkType.Short);
|
|
});
|
|
this.layout.eventHub.on('copyShortLinkToClip', () => {
|
|
this.copyLinkTypeToClipboard(LinkType.Short);
|
|
});
|
|
|
|
this.shareLinkDialog.addEventListener('show.bs.modal', this.onOpenModalPane.bind(this));
|
|
this.shareLinkDialog.addEventListener('hidden.bs.modal', this.onCloseModalPane.bind(this));
|
|
|
|
this.layout.eventHub.on('settingsChange', (newSettings: SiteSettings) => {
|
|
this.settings = newSettings;
|
|
});
|
|
|
|
$(window).on('blur', async () => {
|
|
sessionThenLocalStorage.set('gl', JSON.stringify(this.layout.toConfig()));
|
|
if (this.settings.keepMultipleTabs) {
|
|
try {
|
|
const link = await this.getLinkOfType(LinkType.Full);
|
|
window.history.replaceState(null, '', link);
|
|
} catch (e) {
|
|
// This is probably caused by a link that is too long
|
|
SentryCapture(e, 'url update');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private static bindToLinkType(bind: string): LinkType {
|
|
switch (bind) {
|
|
case 'Full':
|
|
return LinkType.Full;
|
|
case 'Short':
|
|
return LinkType.Short;
|
|
case 'Embed':
|
|
return LinkType.Embed;
|
|
default:
|
|
return LinkType.Full;
|
|
}
|
|
}
|
|
|
|
private onOpenModalPane(event: Event): void {
|
|
const modalEvent = event as Modal.Event;
|
|
if (!modalEvent.relatedTarget) {
|
|
throw new Error('No relatedTarget found in modal event');
|
|
}
|
|
|
|
const button = $(modalEvent.relatedTarget);
|
|
const bindStr = button.data('bind') as string;
|
|
const currentBind = Sharing.bindToLinkType(bindStr);
|
|
const modal = $(event.currentTarget as HTMLElement);
|
|
const socialSharingElements = modal.find('.socialsharing');
|
|
const permalink = modal.find('.permalink');
|
|
const embedsettings = modal.find('#embedsettings');
|
|
const clipboardButton = modal.find('.clippy');
|
|
|
|
const updatePermaLink = () => {
|
|
socialSharingElements.empty();
|
|
const config = this.layout.toConfig();
|
|
Sharing.getLinks(config, currentBind, (error: any, newUrl: string, extra: string, updateState: boolean) => {
|
|
permalink.off('click');
|
|
if (error || !newUrl) {
|
|
clipboardButton.prop('disabled', true);
|
|
permalink.val(error || 'Error providing URL');
|
|
SentryCapture(error, 'Error providing url');
|
|
} else {
|
|
if (updateState) {
|
|
Sharing.storeCurrentConfig(config, extra);
|
|
}
|
|
clipboardButton.prop('disabled', false);
|
|
permalink.val(newUrl);
|
|
permalink.on('click', () => {
|
|
permalink.trigger('focus').trigger('select');
|
|
});
|
|
if (options.sharingEnabled) {
|
|
Sharing.updateShares(socialSharingElements, newUrl);
|
|
// Disable the links for every share item which does not support embed html as links
|
|
if (currentBind === LinkType.Embed) {
|
|
socialSharingElements.children('.share-no-embeddable').hide().on('click', false);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
const clippyElement = modal.find('button.clippy').get(0);
|
|
if (clippyElement != null) {
|
|
this.clippyButton = new ClipboardJS(clippyElement);
|
|
this.clippyButton.on('success', e => {
|
|
this.displayTooltip(permalink, 'Link copied to clipboard');
|
|
e.clearSelection();
|
|
});
|
|
this.clippyButton.on('error', _e => {
|
|
this.displayTooltip(permalink, 'Error copying to clipboard');
|
|
});
|
|
}
|
|
|
|
if (currentBind === LinkType.Embed) {
|
|
embedsettings.show();
|
|
embedsettings
|
|
.find('input')
|
|
// Off any prev click handlers to avoid multiple events triggering after opening the modal more than once
|
|
.off('click')
|
|
.on('click', () => updatePermaLink());
|
|
} else {
|
|
embedsettings.hide();
|
|
}
|
|
|
|
updatePermaLink();
|
|
}
|
|
|
|
private onCloseModalPane(): void {
|
|
if (this.clippyButton) {
|
|
this.clippyButton.destroy();
|
|
this.clippyButton = null;
|
|
}
|
|
}
|
|
|
|
private initButtons(): void {
|
|
const shareShortCopyToClipBtn = this.shareShort.find('.clip-icon');
|
|
const shareFullCopyToClipBtn = this.shareFull.find('.clip-icon');
|
|
const shareEmbedCopyToClipBtn = this.shareEmbed.find('.clip-icon');
|
|
|
|
shareShortCopyToClipBtn.on('click', e => this.onClipButtonPressed(e, LinkType.Short));
|
|
shareFullCopyToClipBtn.on('click', e => this.onClipButtonPressed(e, LinkType.Full));
|
|
shareEmbedCopyToClipBtn.on('click', e => this.onClipButtonPressed(e, LinkType.Embed));
|
|
|
|
if (options.sharingEnabled) {
|
|
Sharing.updateShares($('#socialshare'), window.location.protocol + '//' + window.location.hostname);
|
|
}
|
|
}
|
|
|
|
private onClipButtonPressed(event: ClickEvent, type: LinkType): boolean {
|
|
// Don't let the modal show up.
|
|
// We need this because the button is a child of the dropdown-item with a data-bs-toggle=modal
|
|
if (Sharing.isNavigatorClipboardAvailable()) {
|
|
this.copyLinkTypeToClipboard(type);
|
|
event.stopPropagation();
|
|
// As we prevented bubbling, the dropdown won't close by itself.
|
|
BootstrapUtils.hideDropdown(this.share);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private getLinkOfType(type: LinkType): Promise<string> {
|
|
const config = this.layout.toConfig();
|
|
return new Promise<string>((resolve, reject) => {
|
|
Sharing.getLinks(config, type, (error: any, newUrl: string, extra: string, updateState: boolean) => {
|
|
if (error || !newUrl) {
|
|
this.displayTooltip(this.shareTooltipTarget, 'Oops, something went wrong');
|
|
SentryCapture(error, 'Getting short link failed');
|
|
reject(
|
|
new Error(
|
|
error
|
|
? `Getting short link failed: ${error}`
|
|
: 'Getting short link failed: no URL returned',
|
|
),
|
|
);
|
|
} else {
|
|
if (updateState) {
|
|
Sharing.storeCurrentConfig(config, extra);
|
|
}
|
|
resolve(newUrl);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private copyLinkTypeToClipboard(type: LinkType): void {
|
|
const config = this.layout.toConfig();
|
|
Sharing.getLinks(config, type, (error: any, newUrl: string, extra: string, updateState: boolean) => {
|
|
if (error || !newUrl) {
|
|
this.displayTooltip(this.shareTooltipTarget, 'Oops, something went wrong');
|
|
SentryCapture(error, 'Getting short link failed');
|
|
} else {
|
|
if (updateState) {
|
|
Sharing.storeCurrentConfig(config, extra);
|
|
}
|
|
this.doLinkCopyToClipboard(type, newUrl);
|
|
}
|
|
});
|
|
}
|
|
|
|
// TODO we can consider using bootstrap's "Toast" support in future.
|
|
private displayTooltip(where: JQuery, message: string): void {
|
|
// First dispose any existing tooltip
|
|
const tooltipEl = where[0];
|
|
if (!tooltipEl) return;
|
|
|
|
const existingTooltip = Tooltip.getInstance(tooltipEl);
|
|
if (existingTooltip) {
|
|
existingTooltip.dispose();
|
|
}
|
|
|
|
// Create and show new tooltip
|
|
try {
|
|
const tooltip = new Tooltip(tooltipEl, {
|
|
placement: 'bottom',
|
|
trigger: 'manual',
|
|
title: message,
|
|
});
|
|
|
|
tooltip.show();
|
|
|
|
// Manual triggering of tooltips does not hide them automatically. This timeout ensures they do
|
|
setTimeout(() => tooltip.hide(), 1500);
|
|
} catch (e) {
|
|
// If element doesn't exist, just silently fail
|
|
console.warn('Could not show tooltip:', e);
|
|
}
|
|
}
|
|
|
|
private openShareModalForType(type: LinkType): void {
|
|
switch (type) {
|
|
case LinkType.Short:
|
|
this.shareShort.trigger('click');
|
|
break;
|
|
case LinkType.Full:
|
|
this.shareFull.trigger('click');
|
|
break;
|
|
case LinkType.Embed:
|
|
this.shareEmbed.trigger('click');
|
|
break;
|
|
}
|
|
}
|
|
|
|
private doLinkCopyToClipboard(type: LinkType, link: string): void {
|
|
if (Sharing.isNavigatorClipboardAvailable()) {
|
|
navigator.clipboard
|
|
.writeText(link)
|
|
.then(() => this.displayTooltip(this.shareTooltipTarget, 'Link copied to clipboard'))
|
|
.catch(() => this.openShareModalForType(type));
|
|
} else {
|
|
this.openShareModalForType(type);
|
|
}
|
|
}
|
|
|
|
public static getLinks(config: any, currentBind: LinkType, done: CallableFunction): void {
|
|
const root = window.httpRoot;
|
|
switch (currentBind) {
|
|
case LinkType.Short:
|
|
Sharing.getShortLink(config, root, done);
|
|
return;
|
|
case LinkType.Full:
|
|
done(null, window.location.origin + root + '#' + serialiseState(config), false);
|
|
return;
|
|
case LinkType.Embed: {
|
|
const options: Record<string, boolean> = {};
|
|
$('#sharelinkdialog input:checked').each((i, element) => {
|
|
options[$(element).data('option')] = true;
|
|
});
|
|
done(null, Sharing.getEmbeddedHtml(config, root, false, options), false);
|
|
return;
|
|
}
|
|
default:
|
|
// Hmmm
|
|
done('Unknown link type', null);
|
|
}
|
|
}
|
|
|
|
private static getShortLink(config: any, root: string, done: CallableFunction): void {
|
|
const useExternalShortener = options.urlShortenService !== 'default';
|
|
const data = JSON.stringify({
|
|
config: useExternalShortener ? serialiseState(config) : config,
|
|
});
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: window.location.origin + root + 'api/shortener',
|
|
dataType: 'json', // Expected
|
|
contentType: 'application/json', // Sent
|
|
data: data,
|
|
success: (result: any) => {
|
|
const pushState = useExternalShortener ? null : result.url;
|
|
done(null, result.url, pushState, true);
|
|
},
|
|
error: err => {
|
|
// Notify the user that we ran into trouble?
|
|
done(err.statusText, null, false);
|
|
},
|
|
cache: true,
|
|
});
|
|
}
|
|
|
|
private static getEmbeddedHtml(
|
|
config: any,
|
|
root: string,
|
|
isReadOnly: boolean,
|
|
extraOptions: Record<string, boolean>,
|
|
): string {
|
|
const embedUrl = Sharing.getEmbeddedUrl(config, root, isReadOnly, extraOptions);
|
|
// The attributes must be double quoted, the full url's rison contains single quotes
|
|
return `<iframe width="800px" height="200px" src="${embedUrl}"></iframe>`;
|
|
}
|
|
|
|
private static getEmbeddedUrl(config: any, root: string, readOnly: boolean, extraOptions: object): string {
|
|
const location = window.location.origin + root;
|
|
const parameters = _.reduce(
|
|
extraOptions,
|
|
(total, value, key): string => {
|
|
if (total === '') {
|
|
total = '?';
|
|
} else {
|
|
total += '&';
|
|
}
|
|
|
|
return total + key + '=' + value;
|
|
},
|
|
'',
|
|
);
|
|
|
|
const path = (readOnly ? 'embed-ro' : 'e') + parameters + '#';
|
|
|
|
return location + path + serialiseState(config);
|
|
}
|
|
|
|
private static storeCurrentConfig(config: any, extra: string): void {
|
|
window.history.pushState(null, '', extra);
|
|
}
|
|
|
|
private static isNavigatorClipboardAvailable(): boolean {
|
|
return (navigator.clipboard as Clipboard | undefined) !== undefined;
|
|
}
|
|
|
|
private static updateShares(container: JQuery, url: string): void {
|
|
const baseTemplate = $('#share-item');
|
|
_.each(shareServices, (service, serviceName) => {
|
|
const newElement = baseTemplate.children('a.share-item').clone();
|
|
if (service.logoClass) {
|
|
newElement.prepend(
|
|
$('<span>').addClass('dropdown-icon me-1').addClass(service.logoClass).prop('title', serviceName),
|
|
);
|
|
}
|
|
if (service.text) {
|
|
newElement.children('span.share-item-text').text(service.text);
|
|
}
|
|
newElement
|
|
.prop('href', service.getLink('Compiler Explorer', url))
|
|
.addClass(service.cssClass)
|
|
.toggleClass('share-no-embeddable', !service.embedValid)
|
|
.appendTo(container);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize sharing functionality based on whether we're in embedded mode or not
|
|
export function initialiseSharing(layout: GoldenLayout, isEmbedded: boolean): void {
|
|
if (isEmbedded) {
|
|
// Just create the base class for state tracking and link updates
|
|
new SharingBase(layout);
|
|
} else {
|
|
// Create full Sharing with all features
|
|
new Sharing(layout);
|
|
}
|
|
}
|