Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use @emotion/server for server-side security prompts #142662

Merged
merged 4 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,11 @@
"@elastic/react-search-ui": "^1.14.0",
"@elastic/request-crypto": "2.0.1",
"@elastic/search-ui-app-search-connector": "^1.14.0",
"@emotion/cache": "^11.9.3",
"@emotion/css": "^11.9.0",
"@emotion/react": "^11.9.3",
"@emotion/serialize": "^1.0.4",
"@emotion/cache": "^11.10.3",
"@emotion/css": "^11.10.0",
"@emotion/react": "^11.10.4",
"@emotion/serialize": "^1.1.0",
"@emotion/server": "^11.10.0",
"@grpc/grpc-js": "^1.6.7",
"@hapi/accept": "^5.0.2",
"@hapi/boom": "^9.1.4",
Expand Down Expand Up @@ -690,8 +691,8 @@
"@elastic/github-checks-reporter": "0.0.20b3",
"@elastic/makelogs": "^6.0.0",
"@elastic/synthetics": "^1.0.0-beta.22",
"@emotion/babel-preset-css-prop": "^11.2.0",
"@emotion/jest": "^11.9.4",
"@emotion/babel-preset-css-prop": "^11.10.0",
"@emotion/jest": "^11.10.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@istanbuljs/schema": "^0.1.2",
"@jest/console": "^26.6.2",
Expand Down
57 changes: 32 additions & 25 deletions x-pack/plugins/security/server/prompt_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import { icon as EuiIconAlert } from '@elastic/eui/lib/components/icon/assets/al
// @ts-expect-error no definitions in component folder
import { appendIconComponentCache } from '@elastic/eui/lib/components/icon/icon';
import createCache from '@emotion/cache';
import createEmotionServer from '@emotion/server/create-instance';
import type { ReactNode } from 'react';
import React from 'react';
import { renderToString } from 'react-dom/server';

import { Fonts } from '@kbn/core-rendering-server-internal';
import type { IBasePath } from '@kbn/core/server';
Expand All @@ -34,6 +36,8 @@ appendIconComponentCache({
alert: EuiIconAlert,
});

const emotionCache = createCache({ key: 'eui' });

interface Props {
buildNumber: number;
basePath: IBasePath;
Expand All @@ -51,6 +55,31 @@ export function PromptPage({
body,
actions,
}: Props) {
const content = (
<I18nProvider>
<EuiProvider colorMode="light" cache={emotionCache}>
<EuiPage paddingSize="none" style={{ minHeight: '100vh' }} data-test-subj="promptPage">
<EuiPageBody>
<EuiPageContent verticalPosition="center" horizontalPosition="center">
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={<h2>{title}</h2>}
body={body}
actions={actions}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</EuiProvider>
</I18nProvider>
);

const { extractCriticalToChunks, constructStyleTagsFromChunks } =
createEmotionServer(emotionCache);
const chunks = extractCriticalToChunks(renderToString(content));
const emotionStyles = constructStyleTagsFromChunks(chunks);

const uiPublicURL = `${basePath.serverBasePath}/ui`;
const regularBundlePath = `${basePath.serverBasePath}/${buildNumber}/bundles`;
const styleSheetPaths = [
Expand All @@ -60,16 +89,12 @@ export function PromptPage({
`${basePath.serverBasePath}/ui/legacy_light_theme.css`,
];

// Emotion SSR styles will be prepended to the <body> and emit a console log warning about :first-child selectors
const emotionCache = createCache({
key: 'css',
prepend: true,
});

return (
<html lang={i18n.getLocale()}>
<head>
<title>Elastic</title>
{/* eslint-disable-next-line react/no-danger */}
<style dangerouslySetInnerHTML={{ __html: `</style>${emotionStyles}` }} />
Comment on lines +96 to +97
Copy link
Member Author

@cee-chen cee-chen Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick explanation on why this creates a blank <style></style> tag:

  1. Since Emotion is returning its styles as one giant HTML string (including specific classes/attributes on its <style> tags that we likely should not ignore), we need to use dangerouslySetInnerHTML

  2. The number of children allowed within the <head> tag is limited, e.g. no <div>s or otherwise handy 'grouping' type tag. If you try to use a tag that normally does not have children, e.g. the <meta> tag, Kibana/React will error with [ERROR][http.server.Kibana] Error: meta is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.

  3. style was therefore the tag that made the most sense, but causes weird/unnecessary nesting behavior, hence the prepended </style>. See this StackOverflow answer for how this works. This ends up creating a blank <style></style> tag, but that really isn't the worst thing in the world, and is an unfortunate necessary evil until React supports dangerouslySetInnerHTML on fragments.

{styleSheetPaths.map((path) => (
<link href={path} rel="stylesheet" key={path} />
))}
Expand All @@ -83,25 +108,7 @@ export function PromptPage({
<meta name="theme-color" content="#ffffff" />
<meta name="color-scheme" content="light dark" />
</head>
<body>
<I18nProvider>
<EuiProvider colorMode="light" cache={emotionCache}>
<EuiPage paddingSize="none" style={{ minHeight: '100vh' }} data-test-subj="promptPage">
<EuiPageBody>
<EuiPageContent verticalPosition="center" horizontalPosition="center">
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={<h2>{title}</h2>}
body={body}
actions={actions}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</EuiProvider>
</I18nProvider>
</body>
<body>{content}</body>
</html>
);
}
Loading