Skip to content

Commit bb86f95

Browse files
mayakonevalglasser
andauthored
Embedded Landing Pages (#6397)
Adds a new "embed" option to the default landing page plugins. This lets you run Explorer (for production graphs) or Sandbox (for development graphs) directly on your graph's origin (with the bulk of the code running in an iframe). Among other things, this lets you interact with your graph without needing to configure CORS. Co-authored-by: David Glasser <glasser@apollographql.com>
1 parent 936ca69 commit bb86f95

File tree

4 files changed

+285
-123
lines changed

4 files changed

+285
-123
lines changed

cspell-dict.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,5 @@ websockets
176176
Wheelock's
177177
xorby
178178
YOURNAME
179+
Embeddable
180+
embeddable

packages/apollo-server-core/src/plugin/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,12 @@ export function ApolloServerPluginLandingPageDisabled(): ApolloServerPlugin {
106106
import type {
107107
ApolloServerPluginLandingPageLocalDefaultOptions,
108108
ApolloServerPluginLandingPageProductionDefaultOptions,
109-
} from './landingPage/default';
109+
} from './landingPage/default/types';
110110
export type {
111111
ApolloServerPluginLandingPageDefaultBaseOptions,
112112
ApolloServerPluginLandingPageLocalDefaultOptions,
113113
ApolloServerPluginLandingPageProductionDefaultOptions,
114-
} from './landingPage/default';
114+
} from './landingPage/default/types';
115115
export function ApolloServerPluginLandingPageLocalDefault(
116116
options?: ApolloServerPluginLandingPageLocalDefaultOptions,
117117
): ApolloServerPlugin {

packages/apollo-server-core/src/plugin/landingPage/default/index.ts

Lines changed: 145 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,31 @@
11
import type { ImplicitlyInstallablePlugin } from '../../../ApolloServer';
2-
3-
export interface ApolloServerPluginLandingPageDefaultBaseOptions {
4-
/**
5-
* By default, the landing page plugin uses the latest version of the landing
6-
* page published to Apollo's CDN. If you'd like to pin the current version,
7-
* pass the SHA served at
8-
* https://apollo-server-landing-page.cdn.apollographql.com/_latest/version.txt
9-
* here.
10-
*/
11-
version?: string;
12-
/**
13-
* Set to false to suppress the footer which explains how to configure the
14-
* landing page.
15-
*/
16-
footer?: boolean;
17-
/**
18-
* Users can configure their landing page to link to Studio Explorer with a
19-
* document loaded in the UI.
20-
*/
21-
document?: string;
22-
/**
23-
* Users can configure their landing page to link to Studio Explorer with
24-
* variables loaded in the UI.
25-
*/
26-
variables?: Record<string, string>;
27-
/**
28-
* Users can configure their landing page to link to Studio Explorer with
29-
* headers loaded in the UI.
30-
*/
31-
headers?: Record<string, string>;
32-
/**
33-
* Users can configure their landing page to link to Studio Explorer with the
34-
* setting to include/exclude cookies loaded in the UI.
35-
*/
36-
includeCookies?: boolean;
37-
// For Apollo use only.
38-
__internal_apolloStudioEnv__?: 'staging' | 'prod';
39-
}
40-
41-
export interface ApolloServerPluginLandingPageLocalDefaultOptions
42-
extends ApolloServerPluginLandingPageDefaultBaseOptions {}
43-
44-
export interface ApolloServerPluginLandingPageProductionDefaultOptions
45-
extends ApolloServerPluginLandingPageDefaultBaseOptions {
46-
/**
47-
* If specified, provide a link (with opt-in auto-redirect) to the Studio page
48-
* for the given graphRef. (You need to explicitly pass this here rather than
49-
* relying on the server's ApolloConfig, because if your server is publicly
50-
* accessible you may not want to display the graph ref publicly.)
51-
*/
52-
graphRef?: string;
53-
}
54-
55-
// The actual config object read by the landing page's React component.
56-
interface LandingPageConfig {
57-
graphRef?: string | undefined;
58-
isProd?: boolean;
59-
apolloStudioEnv?: 'staging' | 'prod';
60-
document?: string;
61-
variables?: Record<string, string>;
62-
headers?: Record<string, string>;
63-
includeCookies?: boolean;
64-
footer?: boolean;
65-
}
2+
import type {
3+
ApolloServerPluginEmbeddedLandingPageProductionDefaultOptions,
4+
ApolloServerPluginLandingPageLocalDefaultOptions,
5+
ApolloServerPluginLandingPageProductionDefaultOptions,
6+
LandingPageConfig,
7+
} from './types';
668

679
export function ApolloServerPluginLandingPageLocalDefault(
6810
options: ApolloServerPluginLandingPageLocalDefaultOptions = {},
6911
): ImplicitlyInstallablePlugin {
70-
// We list known keys explicitly to get better typechecking, but we pass
71-
// through extras in case we've added new keys to the splash page and haven't
72-
// quite updated the plugin yet.
73-
const {
74-
version,
75-
__internal_apolloStudioEnv__,
76-
footer,
77-
document,
78-
variables,
79-
headers,
80-
includeCookies,
81-
...rest
82-
} = options;
83-
return ApolloServerPluginLandingPageDefault(
84-
version,
85-
encodeConfig({
86-
isProd: false,
87-
apolloStudioEnv: __internal_apolloStudioEnv__,
88-
footer,
89-
document,
90-
variables,
91-
headers,
92-
includeCookies,
93-
...rest,
94-
}),
95-
);
12+
const { version, __internal_apolloStudioEnv__, ...rest } = options;
13+
return ApolloServerPluginLandingPageDefault(version, {
14+
isProd: false,
15+
apolloStudioEnv: __internal_apolloStudioEnv__,
16+
...rest,
17+
});
9618
}
9719

9820
export function ApolloServerPluginLandingPageProductionDefault(
9921
options: ApolloServerPluginLandingPageProductionDefaultOptions = {},
10022
): ImplicitlyInstallablePlugin {
101-
// We list known keys explicitly to get better typechecking, but we pass
102-
// through extras in case we've added new keys to the splash page and haven't
103-
// quite updated the plugin yet.
104-
const {
105-
version,
106-
__internal_apolloStudioEnv__,
107-
footer,
108-
document,
109-
variables,
110-
headers,
111-
includeCookies,
112-
graphRef,
113-
...rest
114-
} = options;
115-
return ApolloServerPluginLandingPageDefault(
116-
version,
117-
encodeConfig({
118-
isProd: true,
119-
apolloStudioEnv: __internal_apolloStudioEnv__,
120-
footer,
121-
document,
122-
variables,
123-
headers,
124-
includeCookies,
125-
graphRef,
126-
...rest,
127-
}),
128-
);
23+
const { version, __internal_apolloStudioEnv__, ...rest } = options;
24+
return ApolloServerPluginLandingPageDefault(version, {
25+
isProd: true,
26+
apolloStudioEnv: __internal_apolloStudioEnv__,
27+
...rest,
28+
});
12929
}
13030

13131
// A triple encoding! Wow! First we use JSON.stringify to turn our object into a
@@ -140,12 +40,131 @@ function encodeConfig(config: LandingPageConfig): string {
14040
return JSON.stringify(encodeURIComponent(JSON.stringify(config)));
14141
}
14242

43+
// This function turns an object into a string and replaces
44+
// <, >, &, ' with their unicode chars to avoid adding html tags to
45+
// the landing page html that might be passed from the config.
46+
// The only place these characters can appear in the output of
47+
// JSON.stringify is within string literals, where they can equally
48+
// well appear \u-escaped. This specifically means that
49+
// `</script>` won't terminate the script block early.
50+
// (Perhaps we should have done this instead of the triple-encoding
51+
// of encodeConfig for the main landing page.)
52+
function getConfigStringForHtml(config: LandingPageConfig) {
53+
return JSON.stringify(config)
54+
.replace('<', '\\u003c')
55+
.replace('>', '\\u003e')
56+
.replace('&', '\\u0026')
57+
.replace("'", '\\u0027');
58+
}
59+
60+
const getEmbeddedExplorerHTML = (
61+
version: string,
62+
config: ApolloServerPluginEmbeddedLandingPageProductionDefaultOptions,
63+
) => {
64+
interface EmbeddableExplorerOptions {
65+
graphRef: string;
66+
target: string;
67+
68+
initialState?: {
69+
document?: string;
70+
variables?: Record<string, any>;
71+
headers?: Record<string, string>;
72+
displayOptions: {
73+
docsPanelState?: 'open' | 'closed'; // default to 'open',
74+
showHeadersAndEnvVars?: boolean; // default to `false`
75+
theme?: 'dark' | 'light';
76+
};
77+
};
78+
persistExplorerState?: boolean; // defaults to 'false'
79+
80+
endpointUrl: string;
81+
}
82+
const productionLandingPageConfigOrDefault = {
83+
displayOptions: {},
84+
persistExplorerState: false,
85+
...(typeof config.embed === 'boolean' ? {} : config.embed),
86+
};
87+
const embeddedExplorerParams: Omit<EmbeddableExplorerOptions, 'endpointUrl'> =
88+
{
89+
...config,
90+
target: '#embeddableExplorer',
91+
initialState: {
92+
...config,
93+
displayOptions: {
94+
...productionLandingPageConfigOrDefault.displayOptions,
95+
},
96+
},
97+
persistExplorerState:
98+
productionLandingPageConfigOrDefault.persistExplorerState,
99+
};
100+
101+
return `
102+
<style>
103+
iframe {
104+
background-color: white;
105+
}
106+
</style>
107+
<div
108+
style="width: 100vw; height: 100vh; position: absolute; top: 0;"
109+
id="embeddableExplorer"
110+
></div>
111+
<script src="https://embeddable-explorer.cdn.apollographql.com/${version}/embeddable-explorer.umd.production.min.js"></script>
112+
<script>
113+
var endpointUrl = window.location.href;
114+
var embeddedExplorerConfig = ${getConfigStringForHtml(
115+
embeddedExplorerParams,
116+
)};
117+
new window.EmbeddedExplorer({
118+
...embeddedExplorerConfig,
119+
endpointUrl,
120+
});
121+
</script>
122+
`;
123+
};
124+
125+
const getEmbeddedSandboxHTML = (version: string) => {
126+
return `
127+
<style>
128+
iframe {
129+
background-color: white;
130+
}
131+
</style>
132+
<div
133+
style="width: 100vw; height: 100vh; position: absolute; top: 0;"
134+
id="embeddableSandbox"
135+
></div>
136+
<script src="https://embeddable-sandbox.cdn.apollographql.com/${version}/embeddable-sandbox.umd.production.min.js"></script>
137+
<script>
138+
var initialEndpoint = window.location.href;
139+
new window.EmbeddedSandbox({
140+
target: '#embeddableSandbox',
141+
initialEndpoint,
142+
});
143+
</script>
144+
`;
145+
};
146+
147+
const getNonEmbeddedLandingPageHTML = (
148+
version: string,
149+
config: LandingPageConfig,
150+
) => {
151+
const encodedConfig = encodeConfig(config);
152+
153+
return `
154+
<script>window.landingPage = ${encodedConfig};</script>
155+
<script src="https://apollo-server-landing-page.cdn.apollographql.com/${version}/static/js/main.js"></script>`;
156+
};
157+
143158
// Helper for the two actual plugin functions.
144159
function ApolloServerPluginLandingPageDefault(
145160
maybeVersion: string | undefined,
146-
encodedConfig: string,
161+
config: LandingPageConfig & {
162+
isProd: boolean;
163+
apolloStudioEnv: 'staging' | 'prod' | undefined;
164+
},
147165
): ImplicitlyInstallablePlugin {
148166
const version = maybeVersion ?? '_latest';
167+
149168
return {
150169
__internal_installed_implicitly__: false,
151170
async serverWillStart() {
@@ -203,9 +222,14 @@ curl --request POST \\
203222
--url '<script>document.write(window.location.href)</script>' \\
204223
--data '{"query":"query { __typename }"}'</code>
205224
</div>
225+
${
226+
config.embed
227+
? 'graphRef' in config && config.graphRef
228+
? getEmbeddedExplorerHTML(version, config)
229+
: getEmbeddedSandboxHTML(version)
230+
: getNonEmbeddedLandingPageHTML(version, config)
231+
}
206232
</div>
207-
<script>window.landingPage = ${encodedConfig};</script>
208-
<script src="https://apollo-server-landing-page.cdn.apollographql.com/${version}/static/js/main.js"></script>
209233
</body>
210234
</html>
211235
`;

0 commit comments

Comments
 (0)