Preparation for High Contrast Mode, Security domains (#202609)

## Summary

**Reviewers: Please test the code paths affected by this PR. See the
"Risks" section below.**

Part of work for enabling "high contrast mode" in Kibana. See
https://github.com/elastic/kibana/issues/176219.

**Background:**
Kibana will soon have a user profile setting to allow users to enable
"high contrast mode." This setting will activate a flag with
`<EuiProvider>` that causes EUI components to render with higher
contrast visual elements. Consumer plugins and packages need to be
updated selected places where `<EuiProvider>` is wrapped, to pass the
`UserProfileService` service dependency from the CoreStart contract.

**NOTE:** **EUI currently does not yet support the high-contrast mode
flag**, but support for that is expected to come in around 2 weeks.
These first PRs are simply preparing the code by wiring up the
`UserProvideService`.

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [X] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [X] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [ ] [medium/high] The implementor of this change did not manually test
the affected code paths and relied on type-checking and functional tests
to drive the changes. Code owners for this PR need to manually test the
affected code paths.
- [ ] [medium] The `UserProfileService` dependency comes from the
CoreStart contract. If acquiring the service causes synchronous code to
become asynchronous, check for race conditions or errors in rendering
React components. Code owners for this PR need to manually test the
affected code paths.
This commit is contained in:
Tim Sullivan 2024-12-09 13:03:23 -07:00 committed by GitHub
parent 70a5bb33c4
commit 598d3defd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 61 additions and 20 deletions

View file

@ -49,7 +49,7 @@ export const plugin: PluginInitializer<
]);
ReactDOM.render(
<KibanaThemeProvider theme={coreStart.theme}>
<KibanaThemeProvider {...coreStart}>
<KibanaContextProvider services={coreStart}>
<I18nProvider>
<LoginPage />
@ -69,7 +69,7 @@ export const plugin: PluginInitializer<
order: 4000 + 1, // Make sure it comes after the user menu
mount: (element: HTMLElement) => {
ReactDOM.render(
<KibanaThemeProvider theme={coreStart.theme}>
<KibanaThemeProvider {...coreStart}>
<KibanaContextProvider services={coreStart}>
<I18nProvider>
<RoleSwitcher />

View file

@ -13,6 +13,7 @@ import React from 'react';
import type { I18nStart } from '@kbn/core-i18n-browser';
import type { ToastInput } from '@kbn/core-notifications-browser';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { UserProfileService } from '@kbn/core-user-profile-browser';
import { toMountPoint } from '@kbn/react-kibana-mount';
import type { AuthenticatedUser } from '@kbn/security-plugin-types-common';
@ -26,6 +27,7 @@ export const DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON = 'pageReloadButton';
*/
export const createReloadPageToast = (options: {
user: Pick<AuthenticatedUser, 'roles'>;
userProfile: UserProfileService;
theme: ThemeServiceStart;
i18n: I18nStart;
}): ToastInput => {
@ -43,7 +45,7 @@ export const createReloadPageToast = (options: {
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>,
{ i18n: options.i18n, theme: options.theme }
options
),
color: 'success',
toastLifeTimeMs: 0x7fffffff, // Do not auto-hide toast since page is in an unknown state

View file

@ -69,6 +69,7 @@ export const RoleSwitcher = () => {
services.notifications.toasts.add(
createReloadPageToast({
user: authenticateUserState.value,
userProfile: services.userProfile,
theme: services.theme,
i18n: services.i18n,
})

View file

@ -26,5 +26,6 @@
"@kbn/mock-idp-utils",
"@kbn/cloud-plugin",
"@kbn/es",
"@kbn/core-user-profile-browser",
]
}

View file

@ -13,6 +13,7 @@ import React, { useContext } from 'react';
import type { I18nStart } from '@kbn/core-i18n-browser';
import type { NotificationsStart, ToastOptions } from '@kbn/core-notifications-browser';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { UserProfileService } from '@kbn/core-user-profile-browser';
import type { toMountPoint } from '@kbn/react-kibana-mount';
import type { UserProfileAPIClient } from './types';
@ -47,6 +48,7 @@ export interface UserProfilesKibanaDependencies {
core: {
notifications: NotificationsStart;
theme: ThemeServiceStart;
userProfile: UserProfileService;
i18n: I18nStart;
};
security: {
@ -70,7 +72,7 @@ export const UserProfilesKibanaProvider: FC<PropsWithChildren<UserProfilesKibana
...services
}) => {
const {
core: { notifications, i18n, theme },
core: { notifications, ...startServices },
security: { userProfiles: userProfileApiClient },
toMountPoint: toMountPointUtility,
} = services;
@ -86,7 +88,7 @@ export const UserProfilesKibanaProvider: FC<PropsWithChildren<UserProfilesKibana
notifications.toasts.addSuccess(
{
title,
text: text ? toMountPointUtility(text, { i18n, theme }) : undefined,
text: text ? toMountPointUtility(text, startServices) : undefined,
},
toastOptions
);

View file

@ -21,6 +21,7 @@
"@kbn/react-kibana-mount",
"@kbn/core-i18n-browser",
"@kbn/test-jest-helpers",
"@kbn/core-user-profile-browser",
],
"exclude": [
"target/**/*",

View file

@ -42,4 +42,4 @@ export const plugin: PluginInitializer<
> = (initializerContext: PluginInitializerContext) => new SecurityPlugin(initializerContext);
// services needed for rendering React using shared modules
export type StartServices = Pick<CoreStart, 'analytics' | 'i18n' | 'theme'>;
export type StartServices = Pick<CoreStart, 'analytics' | 'i18n' | 'theme' | 'userProfile'>;

View file

@ -15,6 +15,7 @@ import { coreMock, scopedHistoryMock } from '@kbn/core/public/mocks';
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { KibanaFeature } from '@kbn/features-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
@ -196,6 +197,8 @@ function getProps({
const analyticsMock = analyticsServiceMock.createAnalyticsServiceStart();
const i18nMock = i18nServiceMock.createStartContract();
const themeMock = themeServiceMock.createStartContract();
const userProfileMock = userProfileServiceMock.createStart();
return {
action,
roleName: role?.name,
@ -214,6 +217,7 @@ function getProps({
history: scopedHistoryMock.create(),
spacesApiUi,
buildFlavor,
userProfile: userProfileMock,
theme: themeMock,
i18n: i18nMock,
analytics: analyticsMock,

View file

@ -44,7 +44,7 @@ const waitForRender = async (
describe('<RolesGridPage />', () => {
let apiClientMock: jest.Mocked<PublicMethodsOf<RolesAPIClient>>;
let history: ReturnType<typeof scopedHistoryMock.create>;
const { theme, i18n, analytics, notifications } = coreMock.createStart();
const { userProfile, theme, i18n, analytics, notifications } = coreMock.createStart();
beforeEach(() => {
history = scopedHistoryMock.create();
@ -93,6 +93,7 @@ describe('<RolesGridPage />', () => {
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
userProfile={userProfile}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -115,6 +116,7 @@ describe('<RolesGridPage />', () => {
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
userProfile={userProfile}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -139,6 +141,7 @@ describe('<RolesGridPage />', () => {
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
userProfile={userProfile}
/>
);
await waitForRender(wrapper, (updatedWrapper) => {
@ -157,6 +160,7 @@ describe('<RolesGridPage />', () => {
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
userProfile={userProfile}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -201,6 +205,7 @@ describe('<RolesGridPage />', () => {
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
userProfile={userProfile}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -332,6 +337,7 @@ describe('<RolesGridPage />', () => {
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
userProfile={userProfile}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -441,6 +447,7 @@ describe('<RolesGridPage />', () => {
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
userProfile={userProfile}
readOnly
/>
);

View file

@ -75,9 +75,7 @@ export const RolesGridPage: FC<Props> = ({
readOnly,
buildFlavor,
cloudOrgUrl,
analytics,
theme,
i18n: i18nStart,
...startServices
}) => {
const [roles, setRoles] = useState<Role[]>([]);
const [visibleRoles, setVisibleRoles] = useState<Role[]>([]);
@ -409,9 +407,7 @@ export const RolesGridPage: FC<Props> = ({
notifications={notifications}
rolesAPIClient={rolesAPIClient}
buildFlavor={buildFlavor}
theme={theme}
analytics={analytics}
i18n={i18nStart}
{...startServices}
/>
) : null}

View file

@ -22,6 +22,7 @@ import type {
I18nStart,
MountPoint,
ThemeServiceStart,
UserProfileService,
} from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@ -37,6 +38,7 @@ interface Deps {
analytics: Pick<AnalyticsServiceStart, 'reportEvent'>;
i18n: I18nStart;
theme: Pick<ThemeServiceStart, 'theme$'>;
userProfile: UserProfileService;
}
export const insecureClusterAlertText = (deps: Deps, onDismiss: (persist: boolean) => void) =>

View file

@ -16,6 +16,7 @@ import type {
NotificationsStart,
ThemeServiceStart,
Toast,
UserProfileService,
} from '@kbn/core/public';
import { insecureClusterAlertText, insecureClusterAlertTitle } from './components';
@ -33,6 +34,7 @@ interface StartDeps {
analytics: Pick<AnalyticsServiceStart, 'reportEvent'>;
i18n: I18nStart;
theme: Pick<ThemeServiceStart, 'theme$'>;
userProfile: UserProfileService;
}
const DEFAULT_SECURITY_CHECKUP_STATE = Object.freeze<SecurityCheckupState>({

View file

@ -91,6 +91,7 @@
"@kbn/core-capabilities-server",
"@kbn/core-elasticsearch-server",
"@kbn/core-http-server-utils",
"@kbn/core-user-profile-browser-mocks",
],
"exclude": [
"target/**/*",

View file

@ -16,6 +16,7 @@ import {
overlayServiceMock,
themeServiceMock,
} from '@kbn/core/public/mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { EditSpaceContentTab } from './edit_space_content_tab';
@ -37,6 +38,7 @@ const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
const overlays = overlayServiceMock.createStartContract();
const theme = themeServiceMock.createStartContract();
const userProfile = userProfileServiceMock.createStart();
const i18n = i18nServiceMock.createStartContract();
const logger = loggingSystemMock.createLogger();
@ -62,6 +64,7 @@ const TestComponent: React.FC<React.PropsWithChildren> = ({ children }) => {
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
theme={theme}
userProfile={userProfile}
i18n={i18n}
logger={logger}
>

View file

@ -19,6 +19,7 @@ import {
themeServiceMock,
} from '@kbn/core/public/mocks';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { KibanaFeature } from '@kbn/features-plugin/common';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
@ -43,6 +44,7 @@ const reloadWindow = jest.fn();
const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
const overlays = overlayServiceMock.createStartContract();
const userProfile = userProfileServiceMock.createStart();
const theme = themeServiceMock.createStartContract();
const i18n = i18nServiceMock.createStartContract();
const logger = loggingSystemMock.createLogger();
@ -83,6 +85,7 @@ describe('EditSpaceSettings', () => {
getIsRoleManagementEnabled={() => Promise.resolve(() => undefined)}
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
userProfile={userProfile}
theme={theme}
i18n={i18n}
logger={logger}

View file

@ -16,6 +16,7 @@ import {
overlayServiceMock,
themeServiceMock,
} from '@kbn/core/public/mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { EditSpaceAssignedRolesTab } from './edit_space_roles_tab';
@ -34,6 +35,7 @@ const getPrivilegeAPIClient = getPrivilegeAPIClientMock;
const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
const overlays = overlayServiceMock.createStartContract();
const userProfile = userProfileServiceMock.createStart();
const theme = themeServiceMock.createStartContract();
const i18n = i18nServiceMock.createStartContract();
const logger = loggingSystemMock.createLogger();
@ -77,6 +79,7 @@ describe('EditSpaceAssignedRolesTab', () => {
getIsRoleManagementEnabled={getIsRoleManagementEnabled}
getPrivilegesAPIClient={getPrivilegeAPIClient}
getSecurityLicense={getSecurityLicenseMock}
userProfile={userProfile}
theme={theme}
i18n={i18n}
logger={logger}

View file

@ -33,6 +33,7 @@ export const EditSpaceAssignedRolesTab: FC<Props> = ({ space, features, isReadOn
const {
getUrlForApp,
overlays,
userProfile,
theme,
i18n: i18nStart,
logger,
@ -99,7 +100,7 @@ export const EditSpaceAssignedRolesTab: FC<Props> = ({ space, features, isReadOn
}}
/>
</EditSpaceProvider>,
{ theme, i18n: i18nStart }
{ theme, i18n: i18nStart, userProfile }
),
{
size: 'm',
@ -117,6 +118,7 @@ export const EditSpaceAssignedRolesTab: FC<Props> = ({ space, features, isReadOn
features,
invokeClient,
getUrlForApp,
userProfile,
theme,
i18nStart,
notifications.toasts,

View file

@ -18,6 +18,7 @@ import {
themeServiceMock,
} from '@kbn/core/public/mocks';
import type { ApplicationStart } from '@kbn/core-application-browser';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import {
@ -33,6 +34,7 @@ import { getSecurityLicenseMock } from '../../security_license.mock';
const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
const overlays = overlayServiceMock.createStartContract();
const userProfile = userProfileServiceMock.createStart();
const theme = themeServiceMock.createStartContract();
const i18n = i18nServiceMock.createStartContract();
const logger = loggingSystemMock.createLogger();
@ -55,6 +57,7 @@ const SUTProvider = ({
logger,
i18n,
http,
userProfile,
theme,
overlays,
notifications,

View file

@ -35,7 +35,10 @@ import {
import type { SpacesManager } from '../../../spaces_manager';
export interface EditSpaceProviderRootProps
extends Pick<CoreStart, 'theme' | 'i18n' | 'overlays' | 'http' | 'notifications'> {
extends Pick<
CoreStart,
'userProfile' | 'theme' | 'i18n' | 'overlays' | 'http' | 'notifications'
> {
logger: Logger;
capabilities: ApplicationStart['capabilities'];
getUrlForApp: ApplicationStart['getUrlForApp'];

View file

@ -19,6 +19,7 @@ import {
overlayServiceMock,
themeServiceMock,
} from '@kbn/core/public/mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import type { Role, SecurityLicense } from '@kbn/security-plugin-types-common';
import {
@ -43,6 +44,7 @@ const privilegeAPIClient = createPrivilegeAPIClientMock();
const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
const overlays = overlayServiceMock.createStartContract();
const userProfile = userProfileServiceMock.createStart();
const theme = themeServiceMock.createStartContract();
const i18n = i18nServiceMock.createStartContract();
const logger = loggingSystemMock.createLogger();
@ -89,6 +91,7 @@ const renderPrivilegeRolesForm = ({
logger,
i18n,
http,
userProfile,
theme,
overlays,
notifications,

View file

@ -192,7 +192,7 @@ describe('spacesManagementApp', () => {
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
data-test-subj="kbnRedirectAppLink"
>
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"theme":{"theme$":{}},"i18n":{},"logger":{"context":[]},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true}
Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"userProfile":{},"theme":{"theme$":{}},"i18n":{},"logger":{"context":[]},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true}
</div>
</div>
`);

View file

@ -82,7 +82,7 @@ export const spacesManagementApp = Object.freeze({
text: title,
href: `/`,
};
const { notifications, application, chrome, http, overlays, theme } = coreStart;
const { notifications, application, chrome, http, overlays } = coreStart;
chrome.docTitle.change(title);
@ -160,7 +160,8 @@ export const spacesManagementApp = Object.freeze({
http={http}
overlays={overlays}
notifications={notifications}
theme={theme}
userProfile={coreStart.userProfile}
theme={coreStart.theme}
i18n={coreStart.i18n}
logger={logger}
spacesManager={spacesManager}

View file

@ -249,7 +249,7 @@ export class SpaceSelector extends Component<Props, State> {
}
export const renderSpaceSelectorApp = (
services: Pick<CoreStart, 'analytics' | 'i18n' | 'theme'>,
services: Pick<CoreStart, 'analytics' | 'i18n' | 'theme' | 'userProfile'>,
{ element }: Pick<AppMountParameters, 'element'>,
props: Props
) => {

View file

@ -54,7 +54,8 @@
"@kbn/core-application-browser-mocks",
"@kbn/ui-theme",
"@kbn/core-chrome-browser",
"@kbn/core-lifecycle-server"
"@kbn/core-lifecycle-server",
"@kbn/core-user-profile-browser-mocks",
],
"exclude": [
"target/**/*",