add KibanaThemeProvider support for kibana-app-services (#122370)

add KibanaThemeProvider support for kibana-app-services
This commit is contained in:
Shivindera Singh 2022-01-13 15:30:10 +01:00 committed by GitHub
parent f1f35660f0
commit 6dc31d768d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 718 additions and 233 deletions

View file

@ -41,6 +41,7 @@ import { uiActionsPluginMock } from '../../../../ui_actions/public/mocks';
import { getStubPluginServices } from '../../../../presentation_util/public';
const presentationUtil = getStubPluginServices();
const theme = coreMock.createStart().theme;
const options: DashboardContainerServices = {
// TODO: clean up use of any
@ -55,7 +56,7 @@ const options: DashboardContainerServices = {
uiActions: {} as any,
uiSettings: uiSettingsServiceMock.createStartContract(),
http: coreMock.createStart().http,
theme: coreMock.createStart().theme,
theme,
presentationUtil,
};
@ -251,6 +252,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
overlays={{} as any}
inspector={inspector}
SavedObjectFinder={() => null}
theme={theme}
/>
</presentationUtil.ContextProvider>
</KibanaContextProvider>

View file

@ -96,6 +96,14 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
]
}
tableListTitle="Dashboards"
theme={
Object {
"theme$": Observable {
"_isScalar": false,
"_subscribe": [Function],
},
}
}
toastNotifications={
Object {
"add": [MockFunction],
@ -208,6 +216,14 @@ exports[`after fetch initialFilter 1`] = `
]
}
tableListTitle="Dashboards"
theme={
Object {
"theme$": Observable {
"_isScalar": false,
"_subscribe": [Function],
},
}
}
toastNotifications={
Object {
"add": [MockFunction],
@ -319,6 +335,14 @@ exports[`after fetch renders all table rows 1`] = `
]
}
tableListTitle="Dashboards"
theme={
Object {
"theme$": Observable {
"_isScalar": false,
"_subscribe": [Function],
},
}
}
toastNotifications={
Object {
"add": [MockFunction],
@ -430,6 +454,14 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
]
}
tableListTitle="Dashboards"
theme={
Object {
"theme$": Observable {
"_isScalar": false,
"_subscribe": [Function],
},
}
}
toastNotifications={
Object {
"add": [MockFunction],
@ -552,6 +584,14 @@ exports[`after fetch renders call to action with continue when no dashboards exi
]
}
tableListTitle="Dashboards"
theme={
Object {
"theme$": Observable {
"_isScalar": false,
"_subscribe": [Function],
},
}
}
toastNotifications={
Object {
"add": [MockFunction],
@ -663,6 +703,14 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
]
}
tableListTitle="Dashboards"
theme={
Object {
"theme$": Observable {
"_isScalar": false,
"_subscribe": [Function],
},
}
}
toastNotifications={
Object {
"add": [MockFunction],
@ -744,6 +792,14 @@ exports[`after fetch showWriteControls 1`] = `
]
}
tableListTitle="Dashboards"
theme={
Object {
"theme$": Observable {
"_isScalar": false,
"_subscribe": [Function],
},
}
}
toastNotifications={
Object {
"add": [MockFunction],

View file

@ -297,6 +297,7 @@ export const DashboardListing = ({
listingLimit,
tableColumns,
}}
theme={core.theme}
>
<DashboardUnsavedListing
redirectTo={redirectTo}

View file

@ -161,6 +161,7 @@ export function DashboardTopNav({
overlays: core.overlays,
SavedObjectFinder: getSavedObjectFinder(core.savedObjects, uiSettings),
reportUiCounter: usageCollection?.reportUiCounter,
theme: core.theme,
}),
}));
}
@ -171,6 +172,7 @@ export function DashboardTopNav({
core.notifications,
core.savedObjects,
core.overlays,
core.theme,
uiSettings,
usageCollection,
]);

View file

@ -7,6 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
import { ThemeServiceSetup } from 'kibana/public';
import { toMountPoint } from '../../../kibana_react/public';
import { Action, createAction, IncompatibleActionError } from '../../../ui_actions/public';
import { getOverlays, getIndexPatterns } from '../services';
@ -32,7 +33,8 @@ async function isCompatible(context: ApplyGlobalFilterActionContext) {
export function createFilterAction(
filterManager: FilterManager,
timeFilter: TimefilterContract
timeFilter: TimefilterContract,
theme: ThemeServiceSetup
): Action {
return createAction({
type: ACTION_GLOBAL_APPLY_FILTER,
@ -77,7 +79,8 @@ export function createFilterAction(
overlay.close();
resolve(filterSelection);
}
)
),
{ theme$: theme.theme$ }
),
{
'data-test-subj': 'test',

View file

@ -27,6 +27,7 @@ import {
setOverlays,
setSearchService,
setUiSettings,
setTheme,
} from './services';
import { createSearchBar } from './ui/search_bar/create_search_bar';
import {
@ -82,6 +83,7 @@ export class DataPublicPlugin
const startServices = createStartServicesGetter(core.getStartServices);
this.usageCollection = usageCollection;
setTheme(core.theme);
const searchService = this.searchService.setup(core, {
bfetch,
@ -98,7 +100,7 @@ export class DataPublicPlugin
uiActions.registerTrigger(applyFilterTrigger);
uiActions.registerAction(
createFilterAction(queryService.filterManager, queryService.timefilter.timefilter)
createFilterAction(queryService.filterManager, queryService.timefilter.timefilter, core.theme)
);
inspector.registerView(

View file

@ -13,6 +13,7 @@ import { handleResponse } from './handle_response';
import { notificationServiceMock } from '../../../../../core/public/notifications/notifications_service.mock';
import { setNotifications } from '../../services';
import { IKibanaSearchResponse } from 'src/plugins/data/common';
import { themeServiceMock } from 'src/core/public/mocks';
jest.mock('@kbn/i18n', () => {
return {
@ -22,6 +23,8 @@ jest.mock('@kbn/i18n', () => {
};
});
const theme = themeServiceMock.createStartContract();
describe('handleResponse', () => {
const notifications = notificationServiceMock.createStartContract();
@ -37,7 +40,7 @@ describe('handleResponse', () => {
timed_out: true,
},
} as IKibanaSearchResponse<any>;
const result = handleResponse(request, response);
const result = handleResponse(request, response, theme);
expect(result).toBe(response);
expect(notifications.toasts.addWarning).toBeCalled();
expect((notifications.toasts.addWarning as jest.Mock).mock.calls[0][0].title).toMatch(
@ -57,7 +60,7 @@ describe('handleResponse', () => {
},
},
} as IKibanaSearchResponse<any>;
const result = handleResponse(request, response);
const result = handleResponse(request, response, theme);
expect(result).toBe(response);
expect(notifications.toasts.addWarning).toBeCalled();
expect((notifications.toasts.addWarning as jest.Mock).mock.calls[0][0].title).toMatch(
@ -70,7 +73,7 @@ describe('handleResponse', () => {
const response = {
rawResponse: {},
} as IKibanaSearchResponse<any>;
const result = handleResponse(request, response);
const result = handleResponse(request, response, theme);
expect(result).toBe(response);
});
});

View file

@ -11,11 +11,16 @@ import { i18n } from '@kbn/i18n';
import { EuiSpacer } from '@elastic/eui';
import { IKibanaSearchResponse } from 'src/plugins/data/common';
import { ShardFailureOpenModalButton } from '../../ui/shard_failure_modal';
import { ThemeServiceStart } from '../../../../../core/public';
import { toMountPoint } from '../../../../kibana_react/public';
import { getNotifications } from '../../services';
import type { SearchRequest } from '..';
export function handleResponse(request: SearchRequest, response: IKibanaSearchResponse) {
export function handleResponse(
request: SearchRequest,
response: IKibanaSearchResponse,
theme: ThemeServiceStart
) {
const { rawResponse } = response;
if (rawResponse.timed_out) {
@ -45,8 +50,14 @@ export function handleResponse(request: SearchRequest, response: IKibanaSearchRe
<>
{description}
<EuiSpacer size="s" />
<ShardFailureOpenModalButton request={request.body} response={rawResponse} title={title} />
</>
<ShardFailureOpenModalButton
request={request.body}
response={rawResponse}
theme={theme}
title={title}
/>
</>,
{ theme$: theme.theme$ }
);
getNotifications().toasts.addWarning({ title, text });

View file

@ -8,7 +8,7 @@
import type { MockedKeys } from '@kbn/utility-types/jest';
import { CoreSetup, CoreStart } from '../../../../../core/public';
import { coreMock } from '../../../../../core/public/mocks';
import { coreMock, themeServiceMock } from '../../../../../core/public/mocks';
import { IEsSearchRequest } from '../../../common/search';
import { SearchInterceptor } from './search_interceptor';
import { AbortError } from '../../../../kibana_utils/public';
@ -120,6 +120,7 @@ describe('SearchInterceptor', () => {
uiSettings: mockCoreSetup.uiSettings,
http: mockCoreSetup.http,
session: sessionService,
theme: themeServiceMock.createSetupContract(),
});
});

View file

@ -21,7 +21,7 @@ import {
tap,
} from 'rxjs/operators';
import { PublicMethodsOf } from '@kbn/utility-types';
import { CoreSetup, CoreStart, ToastsSetup } from 'kibana/public';
import { CoreSetup, CoreStart, ThemeServiceSetup, ToastsSetup } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { BatchedFunc, BfetchPublicSetup } from 'src/plugins/bfetch/public';
import {
@ -60,6 +60,7 @@ export interface SearchInterceptorDeps {
toasts: ToastsSetup;
usageCollector?: SearchUsageCollector;
session: ISessionService;
theme: ThemeServiceSetup;
}
const MAX_CACHE_ITEMS = 50;
@ -377,7 +378,7 @@ export class SearchInterceptor {
private showTimeoutErrorToast = (e: SearchTimeoutError, sessionId?: string) => {
this.deps.toasts.addDanger({
title: 'Timed out',
text: toMountPoint(e.getErrorMessage(this.application)),
text: toMountPoint(e.getErrorMessage(this.application), { theme$: this.deps.theme.theme$ }),
});
};
@ -392,7 +393,9 @@ export class SearchInterceptor {
this.deps.toasts.addWarning(
{
title: 'Your search session is still running',
text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)),
text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks), {
theme$: this.deps.theme.theme$,
}),
},
{
toastLifeTimeMs: 60000,
@ -423,14 +426,14 @@ export class SearchInterceptor {
title: i18n.translate('data.search.esErrorTitle', {
defaultMessage: 'Cannot retrieve search results',
}),
text: toMountPoint(e.getErrorMessage(this.application)),
text: toMountPoint(e.getErrorMessage(this.application), { theme$: this.deps.theme.theme$ }),
});
} else if (e.constructor.name === 'HttpFetchError') {
this.deps.toasts.addDanger({
title: i18n.translate('data.search.httpErrorTitle', {
defaultMessage: 'Cannot retrieve your data',
}),
text: toMountPoint(getHttpError(e.message)),
text: toMountPoint(getHttpError(e.message), { theme$: this.deps.theme.theme$ }),
});
} else {
this.deps.toasts.addError(e, {

View file

@ -46,7 +46,7 @@ import {
esRawResponse,
} from '../../common/search';
import { AggsService, AggsStartDependencies } from './aggs';
import { IndexPatternsContract } from '..';
import { IKibanaSearchResponse, IndexPatternsContract, SearchRequest } from '..';
import { ISearchInterceptor, SearchInterceptor } from './search_interceptor';
import { SearchUsageCollector, createUsageCollector } from './collectors';
import { UsageCollectionSetup } from '../../../usage_collection/public';
@ -88,7 +88,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
constructor(private initializerContext: PluginInitializerContext<ConfigSchema>) {}
public setup(
{ http, getStartServices, notifications, uiSettings }: CoreSetup,
{ http, getStartServices, notifications, uiSettings, theme }: CoreSetup,
{ bfetch, expressions, usageCollection, nowProvider }: SearchServiceSetupDependencies
): ISearchSetup {
this.usageCollector = createUsageCollector(getStartServices, usageCollection);
@ -112,6 +112,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
startServices: getStartServices(),
usageCollector: this.usageCollector!,
session: this.sessionService,
theme,
});
expressions.registerFunction(
@ -173,7 +174,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
}
public start(
{ http, uiSettings }: CoreStart,
{ http, theme, uiSettings }: CoreStart,
{ fieldFormats, indexPatterns }: SearchServiceStartDependencies
): ISearchStart {
const search = ((request, options = {}) => {
@ -186,7 +187,8 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
const searchSourceDependencies: SearchSourceDependencies = {
getConfig: uiSettings.get.bind(uiSettings),
search,
onResponse: handleResponse,
onResponse: (request: SearchRequest, response: IKibanaSearchResponse) =>
handleResponse(request, response, theme),
};
return {

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { NotificationsStart, CoreStart } from 'src/core/public';
import { NotificationsStart, CoreStart, ThemeServiceStart } from 'src/core/public';
import { createGetterSetter } from '../../kibana_utils/public';
import { IndexPatternsContract } from './data_views';
import { DataPublicPluginStart } from './types';
@ -24,3 +24,5 @@ export const [getIndexPatterns, setIndexPatterns] =
export const [getSearchService, setSearchService] =
createGetterSetter<DataPublicPluginStart['search']>('Search');
export const [getTheme, setTheme] = createGetterSetter<ThemeServiceStart>('Theme');

View file

@ -24,7 +24,6 @@ import {
EuiSuperUpdateButton,
OnRefreshProps,
} from '@elastic/eui';
import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..';
import { useKibana, withKibana } from '../../../../kibana_react/public';
import QueryStringInputUI from './query_string_input';

View file

@ -40,6 +40,7 @@ import type { SuggestionsListSize } from '../typeahead/suggestions_component';
import { SuggestionsComponent } from '..';
import { getFieldSubtypeNested, KIBANA_USER_QUERY_LANGUAGE_KEY } from '../../../common';
import { onRaf } from '../utils';
import { getTheme } from '../../services';
export interface QueryStringInputProps {
indexPatterns: Array<IIndexPattern | string>;
@ -487,7 +488,8 @@ export default class QueryStringInputUI extends PureComponent<Props, State> {
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</div>,
{ theme$: getTheme().theme$ }
),
});
}

View file

@ -8,18 +8,22 @@
import { openModal } from './shard_failure_open_modal_button.test.mocks';
import React from 'react';
import { themeServiceMock } from 'src/core/public/mocks';
import { mountWithIntl } from '@kbn/test/jest';
import ShardFailureOpenModalButton from './shard_failure_open_modal_button';
import { shardFailureRequest } from './__mocks__/shard_failure_request';
import { shardFailureResponse } from './__mocks__/shard_failure_response';
import { findTestSubject } from '@elastic/eui/lib/test';
const theme = themeServiceMock.createStartContract();
describe('ShardFailureOpenModalButton', () => {
it('triggers the openModal function when "Show details" button is clicked', () => {
const component = mountWithIntl(
<ShardFailureOpenModalButton
request={shardFailureRequest}
response={shardFailureResponse}
theme={theme}
title="test"
/>
);

View file

@ -12,6 +12,7 @@ import { EuiButton, EuiTextAlign } from '@elastic/eui';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getOverlays } from '../../services';
import { ThemeServiceStart } from '../../../../../core/public';
import { toMountPoint } from '../../../../kibana_react/public';
import { ShardFailureModal } from './shard_failure_modal';
import { ShardFailureRequest } from './shard_failure_types';
@ -20,6 +21,7 @@ import { ShardFailureRequest } from './shard_failure_types';
export interface ShardFailureOpenModalButtonProps {
request: ShardFailureRequest;
response: estypes.SearchResponse<any>;
theme: ThemeServiceStart;
title: string;
}
@ -28,6 +30,7 @@ export interface ShardFailureOpenModalButtonProps {
export default function ShardFailureOpenModalButton({
request,
response,
theme,
title,
}: ShardFailureOpenModalButtonProps) {
function onClick() {
@ -38,7 +41,8 @@ export default function ShardFailureOpenModalButton({
response={response}
title={title}
onClose={() => modal.close()}
/>
/>,
{ theme$: theme.theme$ }
),
{
className: 'shardFailureModal',

View file

@ -79,7 +79,8 @@ export const getEditorOpener =
requireTimestampField={requireTimestampField}
/>
</I18nProvider>
</KibanaReactContextProvider>
</KibanaReactContextProvider>,
{ theme$: core.theme.theme$ }
),
{
hideCloseButton: true,

View file

@ -75,7 +75,8 @@ export const getFieldDeleteModalOpener =
fieldsToDelete={fieldsToDelete}
closeModal={closeModal}
confirmDelete={onConfirmDelete}
/>
/>,
{ theme$: core.theme.theme$ }
)
);

View file

@ -128,7 +128,8 @@ export const getFieldEditorOpener =
fieldFormats={fieldFormats}
uiSettings={uiSettings}
/>
</KibanaReactContextProvider>
</KibanaReactContextProvider>,
{ theme$: core.theme.theme$ }
),
{
className: euiFlyoutClassname,

View file

@ -11,7 +11,9 @@ import { shallow } from 'enzyme';
import { IndexPattern } from 'src/plugins/data/public';
import { IndexedFieldItem } from '../../types';
import { Table, renderFieldName, getConflictModalContent } from './table';
import { overlayServiceMock } from 'src/core/public/mocks';
import { overlayServiceMock, themeServiceMock } from 'src/core/public/mocks';
const theme = themeServiceMock.createStartContract();
const indexPattern = {
timeFieldName: 'timestamp',
@ -89,6 +91,7 @@ const renderTable = (
editField={editField}
deleteField={() => {}}
openModal={overlayServiceMock.createStartContract().openModal}
theme={theme}
/>
);

View file

@ -7,7 +7,7 @@
*/
import React, { PureComponent } from 'react';
import { OverlayModalStart } from 'src/core/public';
import { OverlayModalStart, ThemeServiceStart } from 'src/core/public';
import {
EuiIcon,
@ -179,6 +179,7 @@ interface IndexedFieldProps {
editField: (field: IndexedFieldItem) => void;
deleteField: (fieldName: string) => void;
openModal: OverlayModalStart['open'];
theme: ThemeServiceStart;
}
const getItems = (conflictDescriptions: IndexedFieldItem['conflictDescriptions']) => {
@ -311,7 +312,8 @@ export const getConflictModalContent = ({
const getConflictBtn = (
fieldName: string,
conflictDescriptions: IndexedFieldItem['conflictDescriptions'],
openModal: IndexedFieldProps['openModal']
openModal: IndexedFieldProps['openModal'],
theme: ThemeServiceStart
) => {
const onClick = () => {
const overlayRef = openModal(
@ -322,7 +324,8 @@ const getConflictBtn = (
},
fieldName,
conflictDescriptions,
})
}),
{ theme$: theme.theme$ }
)
);
};
@ -355,7 +358,12 @@ export class Table extends PureComponent<IndexedFieldProps> {
<span>
{type === 'conflict' && conflictDescription ? '' : type}
{field.conflictDescriptions
? getConflictBtn(field.name, field.conflictDescriptions, this.props.openModal)
? getConflictBtn(
field.name,
field.conflictDescriptions,
this.props.openModal,
this.props.theme
)
: ''}
</span>
);

View file

@ -8,7 +8,7 @@
import React, { Component } from 'react';
import { createSelector } from 'reselect';
import { OverlayStart } from 'src/core/public';
import { OverlayStart, ThemeServiceStart } from 'src/core/public';
import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public';
import { useKibana } from '../../../../../../plugins/kibana_react/public';
import { Table } from './components/table';
@ -28,6 +28,7 @@ interface IndexedFieldsTableProps {
fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean;
userEditPermission: boolean;
openModal: OverlayStart['openModal'];
theme: ThemeServiceStart;
}
interface IndexedFieldsTableState {
@ -129,6 +130,7 @@ class IndexedFields extends Component<IndexedFieldsTableProps, IndexedFieldsTabl
editField={(field) => this.props.helpers.editField(field.name)}
deleteField={(fieldName) => this.props.helpers.deleteField(fieldName)}
openModal={this.props.openModal}
theme={this.props.theme}
/>
</div>
);

View file

@ -80,7 +80,7 @@ export function Tabs({
location,
refreshFields,
}: TabsProps) {
const { application, uiSettings, docLinks, dataViewFieldEditor, overlays } =
const { application, uiSettings, docLinks, dataViewFieldEditor, overlays, theme } =
useKibana<IndexPatternManagmentContext>().services;
const [fieldFilter, setFieldFilter] = useState<string>('');
const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState<string>('');
@ -236,6 +236,7 @@ export function Tabs({
getFieldInfo,
}}
openModal={overlays.openModal}
theme={theme}
/>
)}
</DeleteRuntimeFieldProvider>
@ -295,6 +296,7 @@ export function Tabs({
DeleteRuntimeFieldProvider,
refreshFields,
overlays,
theme,
]
);

View file

@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n-react';
import { StartServicesAccessor } from 'src/core/public';
import { KibanaContextProvider } from '../../../kibana_react/public';
import { KibanaContextProvider, KibanaThemeProvider } from '../../../kibana_react/public';
import { ManagementAppMountParams } from '../../../management/public';
import {
IndexPatternTableWithRouter,
@ -39,7 +39,7 @@ export async function mountManagementSection(
params: ManagementAppMountParams
) {
const [
{ chrome, application, uiSettings, notifications, overlays, http, docLinks },
{ chrome, application, uiSettings, notifications, overlays, http, docLinks, theme },
{ data, dataViewFieldEditor, dataViewEditor },
indexPatternManagementStart,
] = await getStartServices();
@ -67,25 +67,27 @@ export async function mountManagementSection(
ReactDOM.render(
<KibanaContextProvider services={deps}>
<I18nProvider>
<Router history={params.history}>
<Switch>
<Route path={['/create']}>
<IndexPatternTableWithRouter canSave={canSave} showCreateDialog={true} />
</Route>
<Route path={['/dataView/:id/field/:fieldName', '/dataView/:id/create-field/']}>
<CreateEditFieldContainer />
</Route>
<Route path={['/dataView/:id']}>
<EditIndexPatternContainer />
</Route>
<Redirect path={'/patterns*'} to={'dataView*'} />
<Route path={['/']}>
<IndexPatternTableWithRouter canSave={canSave} />
</Route>
</Switch>
</Router>
</I18nProvider>
<KibanaThemeProvider theme$={theme.theme$}>
<I18nProvider>
<Router history={params.history}>
<Switch>
<Route path={['/create']}>
<IndexPatternTableWithRouter canSave={canSave} showCreateDialog={true} />
</Route>
<Route path={['/dataView/:id/field/:fieldName', '/dataView/:id/create-field/']}>
<CreateEditFieldContainer />
</Route>
<Route path={['/dataView/:id']}>
<EditIndexPatternContainer />
</Route>
<Redirect path={'/patterns*'} to={'dataView*'} />
<Route path={['/']}>
<IndexPatternTableWithRouter canSave={canSave} />
</Route>
</Switch>
</Router>
</I18nProvider>
</KibanaThemeProvider>
</KibanaContextProvider>,
params.element
);

View file

@ -18,7 +18,8 @@ export const onRedirectNoIndexPattern =
(
capabilities: CoreStart['application']['capabilities'],
navigateToApp: CoreStart['application']['navigateToApp'],
overlays: CoreStart['overlays']
overlays: CoreStart['overlays'],
theme: CoreStart['theme']
) =>
() => {
const canManageIndexPatterns = capabilities.management.kibana.indexPatterns;
@ -38,7 +39,9 @@ export const onRedirectNoIndexPattern =
// give them a friendly info message instead of a terse error message
bannerId = overlays.banners.replace(
bannerId,
toMountPoint(<EuiCallOut color="warning" iconType="iInCircle" title={bannerMessage} />)
toMountPoint(<EuiCallOut color="warning" iconType="iInCircle" title={bannerMessage} />, {
theme$: theme.theme$,
})
);
// hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around

View file

@ -45,7 +45,7 @@ export class DataViewsPublicPlugin
core: CoreStart,
{ fieldFormats }: DataViewsPublicStartDependencies
): DataViewsPublicPluginStart {
const { uiSettings, http, notifications, savedObjects, overlays, application } = core;
const { uiSettings, http, notifications, savedObjects, theme, overlays, application } = core;
return new DataViewsService({
uiSettings: new UiSettingsPublicToCommon(uiSettings),
@ -59,7 +59,8 @@ export class DataViewsPublicPlugin
onRedirectNoIndexPattern: onRedirectNoIndexPattern(
application.capabilities,
application.navigateToApp,
overlays
overlays,
theme
),
getCanSave: () => Promise.resolve(application.capabilities.indexPatterns.save === true),
});

View file

@ -122,6 +122,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
onBeforeRedirect() {
getUrlTracker().setTrackedUrl('/');
},
theme: core.theme,
})(e);
}
}
@ -139,6 +140,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
id,
services,
toastNotifications,
core.theme,
]);
useEffect(() => {

View file

@ -9,10 +9,11 @@
import { EuiText, EuiIcon, EuiSpacer } from '@elastic/eui';
import React from 'react';
import ReactDOM from 'react-dom';
import { Markdown } from '../../../../kibana_react/public';
import { KibanaThemeProvider, Markdown } from '../../../../kibana_react/public';
import { Embeddable } from './embeddable';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { IContainer } from '../containers';
import { getTheme } from '../../services';
export const ERROR_EMBEDDABLE_TYPE = 'error';
@ -37,8 +38,13 @@ export class ErrorEmbeddable extends Embeddable<EmbeddableInput, EmbeddableOutpu
public render(dom: HTMLElement) {
const title = typeof this.error === 'string' ? this.error : this.error.message;
this.dom = dom;
ReactDOM.render(
// @ts-ignore
let theme;
try {
theme = getTheme();
} catch (err) {
theme = {};
}
const node = (
<div className="embPanel__error embPanel__content" data-test-subj="embeddableStackError">
<EuiText color="subdued" size="xs">
<EuiIcon type="alert" color="danger" />
@ -49,9 +55,16 @@ export class ErrorEmbeddable extends Embeddable<EmbeddableInput, EmbeddableOutpu
data-test-subj="errorMessageMarkdown"
/>
</EuiText>
</div>,
dom
</div>
);
const content =
theme && theme.theme$ ? (
<KibanaThemeProvider theme$={theme.theme$}>{node}</KibanaThemeProvider>
) : (
node
);
ReactDOM.render(content, dom);
}
public destroy() {

View file

@ -31,7 +31,7 @@ import {
import { inspectorPluginMock } from '../../../../inspector/public/mocks';
import { EuiBadge } from '@elastic/eui';
import { embeddablePluginMock } from '../../mocks';
import { applicationServiceMock } from '../../../../../core/public/mocks';
import { applicationServiceMock, themeServiceMock } from '../../../../../core/public/mocks';
const actionRegistry = new Map<string, Action>();
const triggerRegistry = new Map<string, Trigger>();
@ -44,6 +44,7 @@ const trigger: Trigger = {
};
const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
const applicationMock = applicationServiceMock.createStartContract();
const theme = themeServiceMock.createStartContract();
actionRegistry.set(editModeAction.id, editModeAction);
triggerRegistry.set(trigger.id, trigger);
@ -152,6 +153,7 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => {
overlays={{} as any}
inspector={inspector}
SavedObjectFinder={() => null}
theme={theme}
/>
</I18nProvider>
);
@ -191,6 +193,7 @@ const renderInEditModeAndOpenContextMenu = async (
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
theme={theme}
/>
</I18nProvider>
);
@ -298,6 +301,7 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
theme={theme}
/>
</I18nProvider>
);
@ -360,6 +364,7 @@ test('Panel title customize link does not exist in view mode', async () => {
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
theme={theme}
/>
);
@ -395,6 +400,7 @@ test('Runs customize panel action on title click when in edit mode', async () =>
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
theme={theme}
/>
);
@ -443,6 +449,7 @@ test('Updates when hidePanelTitles is toggled', async () => {
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
theme={theme}
/>
</I18nProvider>
);
@ -497,6 +504,7 @@ test('Check when hide header option is false', async () => {
inspector={inspector}
SavedObjectFinder={() => null}
hideHeader={false}
theme={theme}
/>
</I18nProvider>
);
@ -535,6 +543,7 @@ test('Check when hide header option is true', async () => {
inspector={inspector}
SavedObjectFinder={() => null}
hideHeader={true}
theme={theme}
/>
</I18nProvider>
);
@ -567,6 +576,7 @@ test('Should work in minimal way rendering only the inspector action', async ()
getActions={() => Promise.resolve([])}
inspector={inspector}
hideHeader={false}
theme={theme}
/>
</I18nProvider>
);

View file

@ -12,7 +12,7 @@ import React from 'react';
import { Subscription } from 'rxjs';
import deepEqual from 'fast-deep-equal';
import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions';
import { CoreStart, OverlayStart } from '../../../../../core/public';
import { CoreStart, OverlayStart, ThemeServiceStart } from '../../../../../core/public';
import { toMountPoint } from '../../../../kibana_react/public';
import { UsageCollectionStart } from '../../../../usage_collection/public';
@ -83,6 +83,7 @@ interface Props {
showBadges?: boolean;
showNotifications?: boolean;
containerContext?: EmbeddableContainerContext;
theme: ThemeServiceStart;
}
interface State {
@ -347,8 +348,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
) {
return actions;
}
const createGetUserData = (overlays: OverlayStart) =>
const createGetUserData = (overlays: OverlayStart, theme: ThemeServiceStart) =>
async function getUserData(context: { embeddable: IEmbeddable }) {
return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => {
const session = overlays.openModal(
@ -360,7 +360,8 @@ export class EmbeddablePanel extends React.Component<Props, State> {
resolve({ title, hideTitle });
}}
cancel={() => session.close()}
/>
/>,
{ theme$: theme.theme$ }
),
{
'data-test-subj': 'customizePanel',
@ -373,13 +374,16 @@ export class EmbeddablePanel extends React.Component<Props, State> {
// registry.
return {
...actions,
customizePanelTitle: new CustomizePanelTitleAction(createGetUserData(this.props.overlays)),
customizePanelTitle: new CustomizePanelTitleAction(
createGetUserData(this.props.overlays, this.props.theme)
),
addPanel: new AddPanelAction(
this.props.getEmbeddableFactory,
this.props.getAllEmbeddableFactories,
this.props.overlays,
this.props.notifications,
this.props.SavedObjectFinder,
this.props.theme,
this.props.reportUiCounter
),
removePanel: new RemovePanelAction(),

View file

@ -16,7 +16,7 @@ import {
} from '../../../../test_samples/embeddables/filterable_embeddable';
import { FilterableEmbeddableFactory } from '../../../../test_samples/embeddables/filterable_embeddable_factory';
import { FilterableContainer } from '../../../../test_samples/embeddables/filterable_container';
import { coreMock } from '../../../../../../../../core/public/mocks';
import { coreMock, themeServiceMock } from '../../../../../../../../core/public/mocks';
import { ContactCardEmbeddable } from '../../../../test_samples';
import { EmbeddableStart } from '../../../../../plugin';
import { embeddablePluginMock } from '../../../../../mocks';
@ -25,6 +25,7 @@ import { defaultTrigger } from '../../../../../../../ui_actions/public/triggers'
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const getFactory = doStart().getEmbeddableFactory;
const theme = themeServiceMock.createStartContract();
let container: FilterableContainer;
let embeddable: FilterableEmbeddable;
@ -37,7 +38,8 @@ beforeEach(async () => {
() => [] as any,
start.overlays,
start.notifications,
() => null
() => null,
theme
);
const derivedFilter: MockFilter = {
@ -72,7 +74,8 @@ test('Is not compatible when container is in view mode', async () => {
() => [] as any,
start.overlays,
start.notifications,
() => null
() => null,
theme
);
container.updateInput({ viewMode: ViewMode.VIEW });
expect(

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { Action, ActionExecutionContext } from 'src/plugins/ui_actions/public';
import { NotificationsStart, OverlayStart } from 'src/core/public';
import { NotificationsStart, OverlayStart, ThemeServiceStart } from 'src/core/public';
import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
import { ViewMode } from '../../../../types';
import { openAddPanelFlyout } from './open_add_panel_flyout';
@ -31,6 +31,7 @@ export class AddPanelAction implements Action<ActionContext> {
private readonly overlays: OverlayStart,
private readonly notifications: NotificationsStart,
private readonly SavedObjectFinder: React.ComponentType<any>,
private readonly theme: ThemeServiceStart,
private readonly reportUiCounter?: UsageCollectionStart['reportUiCounter']
) {}
@ -63,6 +64,7 @@ export class AddPanelAction implements Action<ActionContext> {
notifications: this.notifications,
SavedObjectFinder: this.SavedObjectFinder,
reportUiCounter: this.reportUiCounter,
theme: this.theme,
});
}
}

View file

@ -7,7 +7,7 @@
*/
import React from 'react';
import { NotificationsStart, OverlayRef, OverlayStart } from 'src/core/public';
import { NotificationsStart, OverlayRef, OverlayStart, ThemeServiceStart } from 'src/core/public';
import { EmbeddableStart } from '../../../../../plugin';
import { toMountPoint } from '../../../../../../../kibana_react/public';
import { IContainer } from '../../../../containers';
@ -23,6 +23,7 @@ export function openAddPanelFlyout(options: {
SavedObjectFinder: React.ComponentType<any>;
showCreateNewMenu?: boolean;
reportUiCounter?: UsageCollectionStart['reportUiCounter'];
theme: ThemeServiceStart;
}): OverlayRef {
const {
embeddable,
@ -33,6 +34,7 @@ export function openAddPanelFlyout(options: {
SavedObjectFinder,
showCreateNewMenu,
reportUiCounter,
theme,
} = options;
const flyoutSession = overlays.openFlyout(
toMountPoint(
@ -49,7 +51,8 @@ export function openAddPanelFlyout(options: {
reportUiCounter={reportUiCounter}
SavedObjectFinder={SavedObjectFinder}
showCreateNewMenu={showCreateNewMenu}
/>
/>,
{ theme$: theme.theme$ }
),
{
'data-test-subj': 'dashboardAddPanel',

View file

@ -20,7 +20,7 @@ import {
ReferenceOrValueEmbeddable,
} from '.';
import { EmbeddablePublicPlugin } from './plugin';
import { coreMock } from '../../../core/public/mocks';
import { coreMock, themeServiceMock } from '../../../core/public/mocks';
import { UiActionsService } from './lib/ui_actions';
import { CoreStart } from '../../../core/public';
import { Start as InspectorStart } from '../../inspector/public';
@ -43,6 +43,8 @@ interface CreateEmbeddablePanelMockArgs {
SavedObjectFinder: React.ComponentType<any>;
}
const theme = themeServiceMock.createStartContract();
export const createEmbeddablePanelMock = ({
getActions,
getEmbeddableFactory,
@ -64,6 +66,7 @@ export const createEmbeddablePanelMock = ({
overlays={overlays || ({} as any)}
inspector={inspector || ({} as any)}
SavedObjectFinder={SavedObjectFinder || (() => null)}
theme={theme}
/>
);
};

View file

@ -52,6 +52,7 @@ import {
getTelemetryFunction,
} from '../common/lib';
import { getAllMigrations } from '../common/lib/get_all_migrations';
import { setTheme } from './services';
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
@ -119,6 +120,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { uiActions }: EmbeddableSetupDependencies) {
setTheme(core.theme);
bootstrap(uiActions);
return {
@ -184,6 +186,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
inspector={inspector}
SavedObjectFinder={getSavedObjectFinder(core.savedObjects, core.uiSettings)}
containerContext={containerContext}
theme={core.theme}
/>
);

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ThemeServiceSetup } from 'src/core/public';
import { createGetterSetter } from '../../kibana_utils/public';
export const [getTheme, setTheme] = createGetterSetter<ThemeServiceSetup>('Theme');

View file

@ -106,7 +106,8 @@ export class InspectorPublicPlugin implements Plugin<Setup, Start> {
uiSettings: core.uiSettings,
share: startDeps.share,
}}
/>
/>,
{ theme$: core.theme.theme$ }
),
{
'data-test-subj': 'inspectorPanel',

View file

@ -24,8 +24,8 @@ export const createNotifications = (services: KibanaServices): KibanaReactNotifi
throw new TypeError('Could not show notification as notifications service is not available.');
}
services.notifications!.toasts.add({
title: toMountPoint(title),
text: toMountPoint(<>{body || null}</>),
title: toMountPoint(title, { theme$: services.theme?.theme$ }),
text: toMountPoint(<>{body || null}</>, { theme$: services.theme?.theme$ }),
color,
iconType,
toastLifeTimeMs,

View file

@ -20,12 +20,18 @@ export const createReactOverlays = (services: KibanaServices): KibanaReactOverla
const openFlyout: KibanaReactOverlays['openFlyout'] = (node, options?) => {
checkCoreService();
return services.overlays!.openFlyout(toMountPoint(<>{node}</>), options);
return services.overlays!.openFlyout(
toMountPoint(<>{node}</>, { theme$: services.theme?.theme$ }),
options
);
};
const openModal: KibanaReactOverlays['openModal'] = (node, options?) => {
checkCoreService();
return services.overlays!.openModal(toMountPoint(<>{node}</>), options);
return services.overlays!.openModal(
toMountPoint(<>{node}</>, { theme$: services.theme?.theme$ }),
options
);
};
const overlays: KibanaReactOverlays = {

View file

@ -10,6 +10,7 @@ import { EuiEmptyPrompt } from '@elastic/eui';
import { shallowWithIntl } from '@kbn/test/jest';
import { ToastsStart } from 'kibana/public';
import React from 'react';
import { themeServiceMock } from '../../../../../src/core/public/mocks';
import { TableListView } from './table_list_view';
const requiredProps = {
@ -24,6 +25,7 @@ const requiredProps = {
tableCaption: 'test caption',
toastNotifications: {} as ToastsStart,
findItems: jest.fn(() => Promise.resolve({ total: 0, hits: [] })),
theme: themeServiceMock.createStartContract(),
};
describe('TableListView', () => {

View file

@ -20,7 +20,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { HttpFetchError, ToastsStart } from 'kibana/public';
import { ThemeServiceStart, HttpFetchError, ToastsStart } from 'kibana/public';
import { debounce, keyBy, sortBy, uniq } from 'lodash';
import React from 'react';
import { KibanaPageTemplate } from '../page_template';
@ -57,6 +57,7 @@ export interface TableListViewProps<V> {
*/
tableCaption: string;
searchFilters?: SearchFilterConfig[];
theme: ThemeServiceStart;
}
export interface TableListViewState<V> {
@ -177,7 +178,8 @@ class TableListView<V extends {}> extends React.Component<
id="kibana-react.tableListView.listing.unableToDeleteDangerMessage"
defaultMessage="Unable to delete {entityName}(s)"
values={{ entityName: this.props.entityName }}
/>
/>,
{ theme$: this.props.theme.theme$ }
),
text: `${error}`,
});

View file

@ -21,8 +21,9 @@ const defaultTheme: CoreTheme = {
darkMode: false,
};
// IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
// That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.
/* IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.*/
// IMPORTANT: This code has been copied to the `kibana_utils` plugin, to avoid cyclical dependency, any changes here should be applied there too.
export const KibanaThemeProvider: FC<KibanaThemeProviderProps> = ({ theme$, children }) => {
const theme = useObservable(theme$, defaultTheme);

View file

@ -10,8 +10,9 @@ import { COLOR_MODES_STANDARD } from '@elastic/eui';
import type { EuiThemeColorModeStandard } from '@elastic/eui';
import type { CoreTheme } from '../../../../core/public';
// IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
// That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.
/* IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.*/
// IMPORTANT: This code has been copied to the `kibana_utils` plugin, to avoid cyclical dependency, any changes here should be applied there too.
export const getColorMode = (theme: CoreTheme): EuiThemeColorModeStandard => {
return theme.darkMode ? COLOR_MODES_STANDARD.dark : COLOR_MODES_STANDARD.light;

View file

@ -13,7 +13,9 @@ import { EuiLoadingSpinner } from '@elastic/eui';
import ReactDOM from 'react-dom';
import { ApplicationStart, HttpStart, ToastsSetup } from 'kibana/public';
import type { ThemeServiceStart } from '../../../../core/public';
import { SavedObjectNotFound } from '..';
import { KibanaThemeProvider } from '../theme';
const ReactMarkdown = React.lazy(() => import('react-markdown'));
const ErrorRenderer = (props: { children: string }) => (
@ -45,6 +47,7 @@ export function redirectWhenMissing({
mapping,
toastNotifications,
onBeforeRedirect,
theme,
}: {
history: History;
navigateToApp: ApplicationStart['navigateToApp'];
@ -62,6 +65,7 @@ export function redirectWhenMissing({
* Optional callback invoked directly before a redirect is triggered
*/
onBeforeRedirect?: (error: SavedObjectNotFound) => void;
theme: ThemeServiceStart;
}) {
let localMappingObject: Mapping;
@ -92,7 +96,12 @@ export function redirectWhenMissing({
defaultMessage: 'Saved object is missing',
}),
text: (element: HTMLElement) => {
ReactDOM.render(<ErrorRenderer>{error.message}</ErrorRenderer>, element);
ReactDOM.render(
<KibanaThemeProvider theme$={theme.theme$}>
<ErrorRenderer>{error.message}</ErrorRenderer>
</KibanaThemeProvider>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
},
});

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { KibanaThemeProvider } from './kibana_theme_provider';

View file

@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useEuiTheme } from '@elastic/eui';
import type { ReactWrapper } from 'enzyme';
import type { FC } from 'react';
import React, { useEffect } from 'react';
import { act } from 'react-dom/test-utils';
import { BehaviorSubject, of } from 'rxjs';
import { mountWithIntl } from '@kbn/test/jest';
import type { CoreTheme } from 'src/core/public';
import { KibanaThemeProvider } from './kibana_theme_provider';
describe('KibanaThemeProvider', () => {
let euiTheme: ReturnType<typeof useEuiTheme> | undefined;
beforeEach(() => {
euiTheme = undefined;
});
const flushPromises = async () => {
await new Promise<void>(async (resolve, reject) => {
try {
setImmediate(() => resolve());
} catch (error) {
reject(error);
}
});
};
const InnerComponent: FC = () => {
const theme = useEuiTheme();
useEffect(() => {
euiTheme = theme;
}, [theme]);
return <div>foo</div>;
};
const refresh = async (wrapper: ReactWrapper<unknown>) => {
await act(async () => {
await flushPromises();
wrapper.update();
});
};
it('exposes the EUI theme provider', async () => {
const coreTheme: CoreTheme = { darkMode: true };
const wrapper = mountWithIntl(
<KibanaThemeProvider theme$={of(coreTheme)}>
<InnerComponent />
</KibanaThemeProvider>
);
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('DARK');
});
it('propagates changes of the coreTheme observable', async () => {
const coreTheme$ = new BehaviorSubject<CoreTheme>({ darkMode: true });
const wrapper = mountWithIntl(
<KibanaThemeProvider theme$={coreTheme$}>
<InnerComponent />
</KibanaThemeProvider>
);
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('DARK');
await act(async () => {
coreTheme$.next({ darkMode: false });
});
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('LIGHT');
});
});

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EuiThemeProvider } from '@elastic/eui';
import type { FC } from 'react';
import React, { useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';
import type { CoreTheme } from '../../../../core/public';
import { getColorMode } from './utils';
interface KibanaThemeProviderProps {
theme$: Observable<CoreTheme>;
}
const defaultTheme: CoreTheme = {
darkMode: false,
};
/**
* Copied from the `kibana_react` plugin, to avoid cyclical dependency
*/
export const KibanaThemeProvider: FC<KibanaThemeProviderProps> = ({ theme$, children }) => {
const theme = useObservable(theme$, defaultTheme);
const colorMode = useMemo(() => getColorMode(theme), [theme]);
return <EuiThemeProvider colorMode={colorMode}>{children}</EuiThemeProvider>;
};

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getColorMode } from './utils';
describe('getColorMode', () => {
it('returns the correct `colorMode` when `darkMode` is enabled', () => {
expect(getColorMode({ darkMode: true })).toEqual('DARK');
});
it('returns the correct `colorMode` when `darkMode` is disabled', () => {
expect(getColorMode({ darkMode: false })).toEqual('LIGHT');
});
});

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { COLOR_MODES_STANDARD } from '@elastic/eui';
import type { EuiThemeColorModeStandard } from '@elastic/eui';
import type { CoreTheme } from '../../../../core/public';
/**
* Copied from the `kibana_react` plugin, to avoid cyclical dependency
*/
export const getColorMode = (theme: CoreTheme): EuiThemeColorModeStandard => {
return theme.darkMode ? COLOR_MODES_STANDARD.dark : COLOR_MODES_STANDARD.light;
};

View file

@ -14,7 +14,5 @@
"index.ts",
"../../../typings/**/*"
],
"references": [
{ "path": "../../core/tsconfig.json" }
]
"references": [{ "path": "../../core/tsconfig.json" }]
}

View file

@ -8,6 +8,6 @@
"githubTeam": "kibana-app-services"
},
"description": "Adds URL Service and sharing capabilities to Kibana",
"requiredBundles": ["kibanaUtils"],
"requiredBundles": ["kibanaReact", "kibanaUtils"],
"optionalPlugins": []
}

View file

@ -11,7 +11,8 @@ import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n-react';
import { EuiWrappingPopover } from '@elastic/eui';
import { CoreStart, HttpStart } from 'kibana/public';
import { CoreStart, HttpStart, ThemeServiceStart } from 'kibana/public';
import { KibanaThemeProvider } from '../../../kibana_react/public';
import { ShareContextMenu } from '../components/share_context_menu';
import { ShareMenuItem, ShowShareMenuOptions } from '../types';
import { ShareMenuRegistryStart } from './share_menu_registry';
@ -42,6 +43,7 @@ export class ShareMenuManager {
post: core.http.post,
basePath: core.http.basePath.get(),
anonymousAccess,
theme: core.theme,
});
},
};
@ -65,12 +67,14 @@ export class ShareMenuManager {
basePath,
embedUrlParamExtensions,
anonymousAccess,
theme,
showPublicUrlSwitch,
}: ShowShareMenuOptions & {
menuItems: ShareMenuItem[];
post: HttpStart['post'];
basePath: string;
anonymousAccess: AnonymousAccessServiceContract | undefined;
theme: ThemeServiceStart;
}) {
if (this.isOpen) {
this.onClose();
@ -82,30 +86,32 @@ export class ShareMenuManager {
document.body.appendChild(this.container);
const element = (
<I18nProvider>
<EuiWrappingPopover
id="sharePopover"
button={anchorElement}
isOpen={true}
closePopover={this.onClose}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<ShareContextMenu
allowEmbed={allowEmbed}
allowShortUrl={allowShortUrl}
objectId={objectId}
objectType={objectType}
shareMenuItems={menuItems}
sharingData={sharingData}
shareableUrl={shareableUrl}
onClose={this.onClose}
post={post}
basePath={basePath}
embedUrlParamExtensions={embedUrlParamExtensions}
anonymousAccess={anonymousAccess}
showPublicUrlSwitch={showPublicUrlSwitch}
/>
</EuiWrappingPopover>
<KibanaThemeProvider theme$={theme.theme$}>
<EuiWrappingPopover
id="sharePopover"
button={anchorElement}
isOpen={true}
closePopover={this.onClose}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<ShareContextMenu
allowEmbed={allowEmbed}
allowShortUrl={allowShortUrl}
objectId={objectId}
objectType={objectType}
shareMenuItems={menuItems}
sharingData={sharingData}
shareableUrl={shareableUrl}
onClose={this.onClose}
post={post}
basePath={basePath}
embedUrlParamExtensions={embedUrlParamExtensions}
anonymousAccess={anonymousAccess}
showPublicUrlSwitch={showPublicUrlSwitch}
/>
</EuiWrappingPopover>
</KibanaThemeProvider>
</I18nProvider>
);
ReactDOM.render(element, this.container);

View file

@ -9,38 +9,45 @@
import * as React from 'react';
import useObservable from 'react-use/lib/useObservable';
import { EuiPageTemplate } from '@elastic/eui';
import { ThemeServiceSetup } from 'kibana/public';
import { Error } from './error';
import { RedirectManager } from '../redirect_manager';
import { Spinner } from './spinner';
import { KibanaThemeProvider } from '../../../../../kibana_react/public';
export interface PageProps {
manager: Pick<RedirectManager, 'error$'>;
theme: ThemeServiceSetup;
}
export const Page: React.FC<PageProps> = ({ manager }) => {
export const Page: React.FC<PageProps> = ({ manager, theme }) => {
const error = useObservable(manager.error$);
if (error) {
return (
<EuiPageTemplate
template="centeredContent"
pageContentProps={{
color: 'danger',
}}
>
<Error error={error} />
</EuiPageTemplate>
<KibanaThemeProvider theme$={theme.theme$}>
<EuiPageTemplate
template="centeredContent"
pageContentProps={{
color: 'danger',
}}
>
<Error error={error} />
</EuiPageTemplate>
</KibanaThemeProvider>
);
}
return (
<EuiPageTemplate
template="centeredContent"
pageContentProps={{
color: 'primary',
}}
>
<Spinner />
</EuiPageTemplate>
<KibanaThemeProvider theme$={theme.theme$}>
<EuiPageTemplate
template="centeredContent"
pageContentProps={{
color: 'primary',
}}
>
<Spinner />
</EuiPageTemplate>
</KibanaThemeProvider>
);
};

View file

@ -29,7 +29,7 @@ export class RedirectManager {
chromeless: true,
mount: async (params) => {
const { render } = await import('./render');
const unmount = render(params.element, { manager: this });
const unmount = render(params.element, { manager: this, theme: core.theme });
this.onMount(params.history.location.search);
return () => {
unmount();

View file

@ -9,6 +9,7 @@
"include": ["common/**/*", "public/**/*", "server/**/*"],
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../kibana_utils/tsconfig.json" },
{ "path": "../kibana_react/tsconfig.json" },
{ "path": "../kibana_utils/tsconfig.json" }
]
}

View file

@ -11,6 +11,8 @@ import React from 'react';
import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui';
import { EventEmitter } from 'events';
import ReactDOM from 'react-dom';
import { KibanaThemeProvider } from '../../../kibana_react/public';
import { getTheme } from '../services';
let activeSession: ContextMenuSession | null = null;
@ -168,20 +170,22 @@ export function openContextMenu(
};
ReactDOM.render(
<EuiPopover
className="embPanel__optionsMenuPopover"
button={container}
isOpen={true}
closePopover={onClose}
panelPaddingSize="none"
anchorPosition="downRight"
>
<EuiContextMenu
initialPanelId="mainMenu"
panels={panels}
data-test-subj={props['data-test-subj']}
/>
</EuiPopover>,
<KibanaThemeProvider theme$={getTheme().theme$}>
<EuiPopover
className="embPanel__optionsMenuPopover"
button={container}
isOpen={true}
closePopover={onClose}
panelPaddingSize="none"
anchorPosition="downRight"
>
<EuiContextMenu
initialPanelId="mainMenu"
panels={panels}
data-test-subj={props['data-test-subj']}
/>
</EuiPopover>
</KibanaThemeProvider>,
container
);

View file

@ -10,6 +10,7 @@ import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core
import { PublicMethodsOf } from '@kbn/utility-types';
import { UiActionsService } from './service';
import { rowClickTrigger, visualizeFieldTrigger, visualizeGeoFieldTrigger } from './triggers';
import { setTheme } from './services';
export type UiActionsSetup = Pick<
UiActionsService,
@ -29,6 +30,7 @@ export class UiActionsPlugin implements Plugin<UiActionsSetup, UiActionsStart> {
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup): UiActionsSetup {
setTheme(core.theme);
this.service.registerTrigger(rowClickTrigger);
this.service.registerTrigger(visualizeFieldTrigger);
this.service.registerTrigger(visualizeGeoFieldTrigger);

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ThemeServiceSetup } from 'src/core/public';
import { createGetterSetter } from '../../kibana_utils/public';
export const [getTheme, setTheme] = createGetterSetter<ThemeServiceSetup>('Theme');

View file

@ -42,6 +42,7 @@ export const VisualizeListing = () => {
visualizeCapabilities,
dashboardCapabilities,
kbnUrlStateStorage,
theme,
},
} = useKibana<VisualizeServices>();
const { pathname } = useLocation();
@ -201,6 +202,7 @@ export const VisualizeListing = () => {
})}
toastNotifications={toastNotifications}
searchFilters={searchFilters}
theme={theme}
>
{dashboardCapabilities.createNew && (
<>

View file

@ -17,6 +17,7 @@ import type {
ToastsStart,
ScopedHistory,
AppMountParameters,
ThemeServiceStart,
} from 'kibana/public';
import type {
@ -105,6 +106,7 @@ export interface VisualizeServices extends CoreStart {
usageCollection?: UsageCollectionStart;
getKibanaVersion: () => string;
spaces?: SpacesPluginStart;
theme: ThemeServiceStart;
visEditorsRegistry: VisEditorsRegistry;
}

View file

@ -92,5 +92,6 @@ export const redirectToSavedObjectPage = (
onBeforeRedirect() {
setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH);
},
theme: services.theme,
})(error);
};

View file

@ -10,6 +10,13 @@
},
"description": "Example integration code for applications to feature reports.",
"optionalPlugins": [],
"requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode", "share"],
"requiredPlugins": [
"reporting",
"developerExamples",
"kibanaReact",
"navigation",
"screenshotMode",
"share"
],
"requiredBundles": ["screenshotting"]
}

View file

@ -9,6 +9,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, Switch } from 'react-router-dom';
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
import { KibanaThemeProvider } from '../../../../../kibana/src/plugins/kibana_react/public';
import { CaptureTest } from './containers/capture_test';
import { Main } from './containers/main';
import { ApplicationContextProvider } from './application_context';
@ -23,12 +24,14 @@ export const renderApp = (
) => {
ReactDOM.render(
<ApplicationContextProvider forwardedState={forwardedParams}>
<Router history={history}>
<Switch>
<Route path={ROUTES.captureTest} exact render={() => <CaptureTest />} />
<Route render={() => <Main basename={appBasePath} {...coreStart} {...deps} />} />
</Switch>
</Router>
<KibanaThemeProvider theme$={coreStart.theme.theme$}>
<Router history={history}>
<Switch>
<Route path={ROUTES.captureTest} exact render={() => <CaptureTest />} />
<Route render={() => <Main basename={appBasePath} {...coreStart} {...deps} />} />
</Switch>
</Router>
</KibanaThemeProvider>
</ApplicationContextProvider>,
element
);

View file

@ -9,15 +9,15 @@
"public/**/*.tsx",
"server/**/*.ts",
"common/**/*.ts",
"../../../typings/**/*",
"../../../typings/**/*"
],
"exclude": [],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/navigation/tsconfig.json" },
{ "path": "../../../src/plugins/screenshot_mode/tsconfig.json" },
{ "path": "../../../examples/developer_examples/tsconfig.json" },
{ "path": "../../plugins/reporting/tsconfig.json" },
{ "path": "../../plugins/reporting/tsconfig.json" }
]
}

View file

@ -81,7 +81,8 @@ export class DataEnhancedPlugin
usageCollector: this.usageCollector,
tourDisabled: plugins.screenshotMode.isScreenshotMode(),
})
)
),
{ theme$: core.theme.theme$ }
),
});
}

View file

@ -74,7 +74,8 @@ export const createDeleteActionDescriptor = (
onClick: async () => {
const ref = core.overlays.openModal(
toMountPoint(
<DeleteConfirm onActionDismiss={() => ref?.close()} searchSession={uiSession} api={api} />
<DeleteConfirm onActionDismiss={() => ref?.close()} searchSession={uiSession} api={api} />,
{ theme$: core.theme.theme$ }
)
);
await ref.onClose;

View file

@ -81,7 +81,8 @@ export const createExtendActionDescriptor = (
onClick: async () => {
const ref = core.overlays.openModal(
toMountPoint(
<ExtendConfirm onActionDismiss={() => ref?.close()} searchSession={uiSession} api={api} />
<ExtendConfirm onActionDismiss={() => ref?.close()} searchSession={uiSession} api={api} />,
{ theme$: core.theme.theme$ }
)
);
await ref.onClose;

View file

@ -97,7 +97,7 @@ export const createInspectActionDescriptor = (
),
onClick: async () => {
const flyout = <InspectFlyout uiSettings={core.uiSettings} searchSession={uiSession} />;
const overlay = core.overlays.openFlyout(toMountPoint(flyout));
const overlay = core.overlays.openFlyout(toMountPoint(flyout, { theme$: core.theme.theme$ }));
await overlay.onClose;
},
});

View file

@ -113,7 +113,8 @@ export const createRenameActionDescriptor = (
onClick: async () => {
const ref = core.overlays.openModal(
toMountPoint(
<RenameDialog onActionDismiss={() => ref?.close()} api={api} searchSession={uiSession} />
<RenameDialog onActionDismiss={() => ref?.close()} api={api} searchSession={uiSession} />,
{ theme$: core.theme.theme$ }
)
);
await ref.onClose;

View file

@ -102,6 +102,7 @@ export function ListingRoute({
tableListTitle={i18n.translate('xpack.graph.listing.graphsTitle', {
defaultMessage: 'Graphs',
})}
theme={coreStart.theme}
/>
</I18nProvider>
);

View file

@ -6,7 +6,7 @@
*/
import React, { FC, useEffect } from 'react';
import type { CoreStart } from 'kibana/public';
import type { CoreStart, ThemeServiceStart } from 'kibana/public';
import type { UiActionsStart } from 'src/plugins/ui_actions/public';
import type { Start as InspectorStartContract } from 'src/plugins/inspector/public';
import { EuiLoadingChart } from '@elastic/eui';
@ -68,6 +68,7 @@ export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDep
const input = { ...props };
const [embeddable, loading, error] = useEmbeddableFactory({ factory, input });
const hasActions = props.withActions === true;
const theme = core.theme;
if (loading) {
return <EuiLoadingChart />;
@ -81,6 +82,7 @@ export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDep
inspector={inspector}
actionPredicate={() => hasActions}
input={input}
theme={theme}
/>
);
}
@ -95,6 +97,7 @@ interface EmbeddablePanelWrapperProps {
inspector: PluginsStartDependencies['inspector'];
actionPredicate: (id: string) => boolean;
input: EmbeddableComponentProps;
theme: ThemeServiceStart;
}
const EmbeddablePanelWrapper: FC<EmbeddablePanelWrapperProps> = ({
@ -103,6 +106,7 @@ const EmbeddablePanelWrapper: FC<EmbeddablePanelWrapperProps> = ({
actionPredicate,
inspector,
input,
theme,
}) => {
useEffect(() => {
embeddable.updateInput(input);
@ -118,6 +122,7 @@ const EmbeddablePanelWrapper: FC<EmbeddablePanelWrapperProps> = ({
showShadow={false}
showBadges={false}
showNotifications={false}
theme={theme}
/>
);
};

View file

@ -54,6 +54,7 @@ export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging;
export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider;
export const getSecurityService = () => pluginsStart.security;
export const getSpacesApi = () => pluginsStart.spaces;
export const getTheme = () => coreStart.theme;
// xpack.maps.* kibana.yml settings from this plugin
let mapAppConfig: MapsConfigType;

View file

@ -21,6 +21,7 @@ import {
getSavedObjectsClient,
getSavedObjectsTagging,
getSavedObjects,
getTheme,
} from '../../kibana_services';
import { getAppTitle } from '../../../common/i18n_getters';
import { MapSavedObjectAttributes } from '../../../common/map_saved_object_type';
@ -148,6 +149,7 @@ export function MapsListView() {
tableListTitle={getAppTitle()}
toastNotifications={getToasts()}
searchFilters={searchFilters}
theme={getTheme()}
/>
);
}

View file

@ -7,7 +7,7 @@
import sinon, { stub } from 'sinon';
import { NotificationsStart } from 'src/core/public';
import { coreMock } from '../../../../../src/core/public/mocks';
import { coreMock, themeServiceMock } from '../../../../../src/core/public/mocks';
import { JobSummary, ReportApiJSON } from '../../common/types';
import { Job } from './job';
import { ReportingAPIClient } from './reporting_api_client';
@ -46,19 +46,21 @@ const notificationsMock = {
},
} as unknown as NotificationsStart;
const theme = themeServiceMock.createStartContract();
describe('stream handler', () => {
afterEach(() => {
sinon.reset();
});
it('constructs', () => {
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
expect(sh).not.toBe(null);
});
describe('findChangedStatusJobs', () => {
it('finds no changed status jobs from empty', (done) => {
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
const findJobs = sh.findChangedStatusJobs([]);
findJobs.subscribe((data) => {
expect(data).toEqual({ completed: [], failed: [] });
@ -67,7 +69,7 @@ describe('stream handler', () => {
});
it('finds changed status jobs', (done) => {
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
const findJobs = sh.findChangedStatusJobs([
'job-source-mock1',
'job-source-mock2',
@ -83,7 +85,7 @@ describe('stream handler', () => {
describe('showNotifications', () => {
it('show success', (done) => {
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [
{
@ -104,7 +106,7 @@ describe('stream handler', () => {
});
it('show max length warning', (done) => {
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [
{
@ -126,7 +128,7 @@ describe('stream handler', () => {
});
it('show csv formulas warning', (done) => {
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [
{
@ -148,7 +150,7 @@ describe('stream handler', () => {
});
it('show failed job toast', (done) => {
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [],
failed: [
@ -169,7 +171,7 @@ describe('stream handler', () => {
});
it('show multiple toast', (done) => {
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [
{

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import * as Rx from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { NotificationsSetup } from 'src/core/public';
import { NotificationsSetup, ThemeServiceStart } from 'src/core/public';
import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUSES } from '../../common/constants';
import { JobId, JobSummary, JobSummarySet } from '../../common/types';
import {
@ -37,7 +37,11 @@ function getReportStatus(src: Job): JobSummary {
}
export class ReportingNotifierStreamHandler {
constructor(private notifications: NotificationsSetup, private apiClient: ReportingAPIClient) {}
constructor(
private notifications: NotificationsSetup,
private apiClient: ReportingAPIClient,
private theme: ThemeServiceStart
) {}
/*
* Use Kibana Toast API to show our messages
@ -54,7 +58,8 @@ export class ReportingNotifierStreamHandler {
getWarningFormulasToast(
job,
this.apiClient.getManagementLink,
this.apiClient.getDownloadLink
this.apiClient.getDownloadLink,
this.theme
)
);
} else if (job.maxSizeReached) {
@ -62,12 +67,18 @@ export class ReportingNotifierStreamHandler {
getWarningMaxSizeToast(
job,
this.apiClient.getManagementLink,
this.apiClient.getDownloadLink
this.apiClient.getDownloadLink,
this.theme
)
);
} else {
this.notifications.toasts.addSuccess(
getSuccessToast(job, this.apiClient.getManagementLink, this.apiClient.getDownloadLink)
getSuccessToast(
job,
this.apiClient.getManagementLink,
this.apiClient.getDownloadLink,
this.theme
)
);
}
}
@ -76,7 +87,7 @@ export class ReportingNotifierStreamHandler {
for (const job of failedJobs) {
const errorText = await this.apiClient.getError(job.id);
this.notifications.toasts.addDanger(
getFailureToast(errorText, job, this.apiClient.getManagementLink)
getFailureToast(errorText, job, this.apiClient.getManagementLink, this.theme)
);
}
return { completed: completedJobs, failed: failedJobs };
@ -120,7 +131,8 @@ export class ReportingNotifierStreamHandler {
i18n.translate('xpack.reporting.publicNotifier.httpErrorMessage', {
defaultMessage: 'Could not check Reporting job status!',
}),
err
err,
this.theme
)
); // prettier-ignore
window.console.error(err);

View file

@ -8,10 +8,14 @@
import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { ToastInput } from 'src/core/public';
import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
export const getGeneralErrorToast = (errorText: string, err: Error): ToastInput => ({
export const getGeneralErrorToast = (
errorText: string,
err: Error,
theme: ThemeServiceStart
): ToastInput => ({
text: toMountPoint(
<Fragment>
<EuiCallOut title={errorText} color="danger" iconType="alert">
@ -24,7 +28,8 @@ export const getGeneralErrorToast = (errorText: string, err: Error): ToastInput
id="xpack.reporting.publicNotifier.error.tryRefresh"
defaultMessage="Try refreshing the page."
/>
</Fragment>
</Fragment>,
{ theme$: theme.theme$ }
),
iconType: undefined,
});

View file

@ -9,14 +9,15 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { Fragment } from 'react';
import { ToastInput } from 'src/core/public';
import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobSummary, ManagementLinkFn } from '../../common/types';
export const getFailureToast = (
errorText: string,
job: JobSummary,
getManagmenetLink: ManagementLinkFn
getManagmenetLink: ManagementLinkFn,
theme: ThemeServiceStart
): ToastInput => {
return {
title: toMountPoint(
@ -24,7 +25,8 @@ export const getFailureToast = (
id="xpack.reporting.publicNotifier.error.couldNotCreateReportTitle"
defaultMessage="Could not create report for {reportObjectType} '{reportObjectTitle}'."
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
/>
/>,
{ theme$: theme.theme$ }
),
text: toMountPoint(
<Fragment>
@ -58,7 +60,8 @@ export const getFailureToast = (
}}
/>
</p>
</Fragment>
</Fragment>,
{ theme$: theme.theme$ }
),
iconType: undefined,
'data-test-subj': 'completeReportFailure',

View file

@ -7,7 +7,7 @@
import { FormattedMessage } from '@kbn/i18n-react';
import React, { Fragment } from 'react';
import { ToastInput } from 'src/core/public';
import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { DownloadButton } from './job_download_button';
@ -16,14 +16,16 @@ import { ReportLink } from './report_link';
export const getSuccessToast = (
job: JobSummary,
getReportLink: () => string,
getDownloadLink: (jobId: JobId) => string
getDownloadLink: (jobId: JobId) => string,
theme: ThemeServiceStart
): ToastInput => ({
title: toMountPoint(
<FormattedMessage
id="xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle"
defaultMessage="Created report for {reportObjectType} '{reportObjectTitle}'"
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
/>
/>,
{ theme$: theme.theme$ }
),
color: 'success',
text: toMountPoint(
@ -32,7 +34,8 @@ export const getSuccessToast = (
<ReportLink getUrl={getReportLink} />
</p>
<DownloadButton getUrl={getDownloadLink} job={job} />
</Fragment>
</Fragment>,
{ theme$: theme.theme$ }
),
'data-test-subj': 'completeReportSuccess',
});

View file

@ -7,7 +7,7 @@
import { FormattedMessage } from '@kbn/i18n-react';
import React, { Fragment } from 'react';
import { ToastInput } from 'src/core/public';
import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { DownloadButton } from './job_download_button';
@ -16,14 +16,16 @@ import { ReportLink } from './report_link';
export const getWarningFormulasToast = (
job: JobSummary,
getReportLink: () => string,
getDownloadLink: (jobId: JobId) => string
getDownloadLink: (jobId: JobId) => string,
theme: ThemeServiceStart
): ToastInput => ({
title: toMountPoint(
<FormattedMessage
id="xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportTitle"
defaultMessage="Report may contain formulas {reportObjectType} '{reportObjectTitle}'"
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
/>
/>,
{ theme$: theme.theme$ }
),
text: toMountPoint(
<Fragment>
@ -37,7 +39,8 @@ export const getWarningFormulasToast = (
<ReportLink getUrl={getReportLink} />
</p>
<DownloadButton getUrl={getDownloadLink} job={job} />
</Fragment>
</Fragment>,
{ theme$: theme.theme$ }
),
'data-test-subj': 'completeReportCsvFormulasWarning',
});

View file

@ -7,7 +7,7 @@
import { FormattedMessage } from '@kbn/i18n-react';
import React, { Fragment } from 'react';
import { ToastInput } from 'src/core/public';
import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { DownloadButton } from './job_download_button';
@ -16,14 +16,16 @@ import { ReportLink } from './report_link';
export const getWarningMaxSizeToast = (
job: JobSummary,
getReportLink: () => string,
getDownloadLink: (jobId: JobId) => string
getDownloadLink: (jobId: JobId) => string,
theme: ThemeServiceStart
): ToastInput => ({
title: toMountPoint(
<FormattedMessage
id="xpack.reporting.publicNotifier.maxSizeReached.partialReportTitle"
defaultMessage="Created partial report for {reportObjectType} '{reportObjectTitle}'"
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
/>
/>,
{ theme$: theme.theme$ }
),
text: toMountPoint(
<Fragment>
@ -37,7 +39,8 @@ export const getWarningMaxSizeToast = (
<ReportLink getUrl={getReportLink} />
</p>
<DownloadButton getUrl={getDownloadLink} job={job} />
</Fragment>
</Fragment>,
{ theme$: theme.theme$ }
),
'data-test-subj': 'completeReportMaxSizeWarning',
});

View file

@ -17,6 +17,7 @@ import {
NotificationsSetup,
Plugin,
PluginInitializerContext,
ThemeServiceStart,
} from 'src/core/public';
import type { ScreenshottingSetup } from '../../screenshotting/public';
import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public';
@ -56,13 +57,18 @@ function getStored(): JobId[] {
return sessionValue ? JSON.parse(sessionValue) : [];
}
function handleError(notifications: NotificationsSetup, err: Error): Rx.Observable<JobSummarySet> {
function handleError(
notifications: NotificationsSetup,
err: Error,
theme: ThemeServiceStart
): Rx.Observable<JobSummarySet> {
notifications.toasts.addDanger(
getGeneralErrorToast(
i18n.translate('xpack.reporting.publicNotifier.pollingErrorMessage', {
defaultMessage: 'Reporting notifier error!',
}),
err
err,
theme
)
);
window.console.error(err);
@ -235,6 +241,7 @@ export class ReportingPublicPlugin
startServices$,
uiSettings,
usesUiCapabilities,
theme: core.theme,
})
);
@ -246,6 +253,7 @@ export class ReportingPublicPlugin
startServices$,
uiSettings,
usesUiCapabilities,
theme: core.theme,
})
);
@ -255,7 +263,7 @@ export class ReportingPublicPlugin
public start(core: CoreStart) {
const { notifications } = core;
const apiClient = this.getApiClient(core.http, core.uiSettings);
const streamHandler = new StreamHandler(notifications, apiClient);
const streamHandler = new StreamHandler(notifications, apiClient, core.theme);
const interval = durationToNumber(this.config.poll.jobsRefresh.interval);
Rx.timer(0, interval)
.pipe(
@ -264,7 +272,7 @@ export class ReportingPublicPlugin
filter((storedJobs) => storedJobs.length > 0), // stop the pipeline here if there are none pending
mergeMap((storedJobs) => streamHandler.findChangedStatusJobs(storedJobs)), // look up the latest status of all pending jobs on the server
mergeMap(({ completed, failed }) => streamHandler.showNotifications({ completed, failed })),
catchError((err) => handleError(notifications, err))
catchError((err) => handleError(notifications, err, core.theme))
)
.subscribe();

View file

@ -6,7 +6,7 @@
*/
import * as Rx from 'rxjs';
import type { IUiSettingsClient, ToastsSetup } from 'src/core/public';
import type { IUiSettingsClient, ThemeServiceSetup, ToastsSetup } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import type { LayoutParams } from '../../../screenshotting/common';
import type { LicensingPluginSetup } from '../../../licensing/public';
@ -19,6 +19,7 @@ export interface ExportPanelShareOpts {
license$: LicensingPluginSetup['license$']; // FIXME: 'license$' is deprecated
startServices$: Rx.Observable<[CoreStart, object, unknown]>;
usesUiCapabilities: boolean;
theme: ThemeServiceSetup;
}
export interface ReportingSharingData {

View file

@ -21,6 +21,7 @@ export const ReportingCsvShareProvider = ({
license$,
startServices$,
usesUiCapabilities,
theme,
}: ExportPanelShareOpts) => {
let licenseToolTipContent = '';
let licenseHasCsvReporting = false;
@ -96,6 +97,7 @@ export const ReportingCsvShareProvider = ({
objectId={objectId}
getJobParams={getJobParams}
onClose={onClose}
theme={theme}
/>
),
},

View file

@ -63,6 +63,7 @@ export const reportingScreenshotShareProvider = ({
license$,
startServices$,
usesUiCapabilities,
theme,
}: ExportPanelShareOpts) => {
let licenseToolTipContent = '';
let licenseDisabled = true;
@ -156,6 +157,7 @@ export const reportingScreenshotShareProvider = ({
getJobParams={getJobParams(apiClient, jobProviderOptions, pngReportType)}
isDirty={isDirty}
onClose={onClose}
theme={theme}
/>
),
},
@ -191,6 +193,7 @@ export const reportingScreenshotShareProvider = ({
getJobParams={getJobParams(apiClient, jobProviderOptions, pdfReportType)}
isDirty={isDirty}
onClose={onClose}
theme={theme}
/>
),
},

View file

@ -10,6 +10,7 @@ import { mountWithIntl } from '@kbn/test/jest';
import {
httpServiceMock,
notificationServiceMock,
themeServiceMock,
uiSettingsServiceMock,
} from 'src/core/public/mocks';
import { ReportingAPIClient } from '../../lib/reporting_api_client';
@ -21,6 +22,8 @@ jest.mock('./constants', () => ({
}));
import * as constants from './constants';
const theme = themeServiceMock.createSetupContract();
describe('ReportingPanelContent', () => {
const props: Partial<Props> = {
layoutId: 'super_cool_layout_id_X',
@ -58,6 +61,7 @@ describe('ReportingPanelContent', () => {
apiClient={apiClient}
toasts={toasts}
uiSettings={uiSettings}
theme={theme}
{...props}
{...newProps}
/>

View file

@ -18,7 +18,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react';
import React, { Component, ReactElement } from 'react';
import { IUiSettingsClient, ToastsSetup } from 'src/core/public';
import { IUiSettingsClient, ThemeServiceSetup, ToastsSetup } from 'src/core/public';
import url from 'url';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
import {
@ -46,6 +46,7 @@ export interface ReportingPanelProps {
options?: ReactElement | null;
isDirty?: boolean;
onClose?: () => void;
theme: ThemeServiceSetup;
}
export type Props = ReportingPanelProps & { intl: InjectedIntl };
@ -291,7 +292,8 @@ class ReportingPanelContentUi extends Component<Props, State> {
</a>
),
}}
/>
/>,
{ theme$: this.props.theme.theme$ }
),
'data-test-subj': 'queueReportSuccess',
});

View file

@ -8,7 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { coreMock } from 'src/core/public/mocks';
import { coreMock, themeServiceMock } from 'src/core/public/mocks';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import { ScreenCapturePanelContent } from './screen_capture_panel_content';
@ -27,6 +27,8 @@ const getJobParamsDefault = () => ({
browserTimezone: 'America/New_York',
});
const theme = themeServiceMock.createSetupContract();
test('ScreenCapturePanelContent renders the default view properly', () => {
const component = mount(
<IntlProvider locale="en">
@ -37,6 +39,7 @@ test('ScreenCapturePanelContent renders the default view properly', () => {
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
theme={theme}
/>
</IntlProvider>
);
@ -56,6 +59,7 @@ test('ScreenCapturePanelContent properly renders a view with "canvas" layout opt
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
theme={theme}
/>
</IntlProvider>
);
@ -75,6 +79,7 @@ test('ScreenCapturePanelContent allows POST URL to be copied when objectId is pr
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
objectId={'1234-5'}
theme={theme}
/>
</IntlProvider>
);
@ -93,6 +98,7 @@ test('ScreenCapturePanelContent does not allow POST URL to be copied when object
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
theme={theme}
/>
</IntlProvider>
);
@ -111,6 +117,7 @@ test('ScreenCapturePanelContent properly renders a view with "print" layout opti
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
theme={theme}
/>
</IntlProvider>
);
@ -130,6 +137,7 @@ test('ScreenCapturePanelContent decorated job params are visible in the POST URL
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
theme={theme}
/>
</IntlProvider>
);

View file

@ -35,6 +35,7 @@ export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClie
apiClient={apiClient}
toasts={core.notifications.toasts}
uiSettings={core.uiSettings}
theme={core.theme}
{...props}
/>
);
@ -48,6 +49,7 @@ export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClie
apiClient={apiClient}
toasts={core.notifications.toasts}
uiSettings={core.uiSettings}
theme={core.theme}
{...props}
/>
);
@ -61,6 +63,7 @@ export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClie
apiClient={apiClient}
toasts={core.notifications.toasts}
uiSettings={core.uiSettings}
theme={core.theme}
{...props}
/>
);

View file

@ -6,18 +6,14 @@
"declaration": true,
"declarationMap": true
},
"include": [
"common/**/*",
"public/**/*",
"server/**/*",
"../../../typings/**/*"
],
"include": ["common/**/*", "public/**/*", "server/**/*", "../../../typings/**/*"],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../../src/plugins/data/tsconfig.json"},
{ "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../../../src/plugins/discover/tsconfig.json" },
{ "path": "../../../src/plugins/embeddable/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
{ "path": "../../../src/plugins/management/tsconfig.json" },
{ "path": "../../../src/plugins/screenshot_mode/tsconfig.json" },
{ "path": "../../../src/plugins/share/tsconfig.json" },
@ -29,6 +25,6 @@
{ "path": "../licensing/tsconfig.json" },
{ "path": "../screenshotting/tsconfig.json" },
{ "path": "../security/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" }
]
}

View file

@ -72,7 +72,7 @@ interface RuntimeField {
type: RuntimeType; // 'long' | 'boolean' ...
script: {
source: string;
}
};
}
```
@ -103,8 +103,8 @@ interface Context {
The runtime field editor is also exported as static React component that you can import into your components. The editor is exported in 2 flavours:
* As the content of a `<EuiFlyout />` (it contains a flyout header and footer)
* As a standalone component that you can inline anywhere
- As the content of a `<EuiFlyout />` (it contains a flyout header and footer)
- As a standalone component that you can inline anywhere
**Note:** The runtime field editor uses the `<CodeEditor />` that has a dependency on the `Provider` from the `"kibana_react"` plugin. If your app is not already wrapped by this provider you will need to add it at least around the runtime field editor. You can see an example in the ["Using the core.overlays.openFlyout()"](#using-the-coreoverlaysopenflyout) example below.
@ -118,7 +118,7 @@ import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields
const MyComponent = () => {
const { docLinksStart } = useCoreContext(); // access the core start service
const [isFlyoutVisilbe, setIsFlyoutVisible] = useState(false);
const saveRuntimeField = useCallback((field: RuntimeField) => {
// Do something with the field
}, []);
@ -139,7 +139,7 @@ const MyComponent = () => {
</EuiFlyout>
)}
</>
)
)
}
```
@ -157,11 +157,11 @@ import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields
const MyComponent = () => {
// Access the core start service
const { docLinksStart, overlays, uiSettings } = useCoreContext();
const { docLinksStart, theme, overlays, uiSettings } = useCoreContext();
const flyoutEditor = useRef<OverlayRef | null>(null);
const { openFlyout } = overlays;
const saveRuntimeField = useCallback((field: RuntimeField) => {
// Do something with the field
}, []);
@ -179,7 +179,8 @@ const MyComponent = () => {
defaultValue={defaultRuntimeField}
ctx={/*optional context object -- see section above*/}
/>
</KibanaReactContextProvider>
</KibanaReactContextProvider>,
{ theme$: theme.theme$ }
)
);
}, [openFlyout, saveRuntimeField, uiSettings]);
@ -188,7 +189,7 @@ const MyComponent = () => {
<>
<EuiButton onClick={openRuntimeFieldEditor}>Create field</EuiButton>
</>
)
)
}
```
@ -208,7 +209,7 @@ const MyComponent = () => {
});
const { submit, isValid: isFormValid, isSubmitted } = runtimeFieldFormState;
const saveRuntimeField = useCallback(async () => {
const { isValid, data } = await submit();
if (isValid) {
@ -233,6 +234,6 @@ const MyComponent = () => {
Save field
</EuiButton>
</>
)
)
}
```
```

View file

@ -22,7 +22,7 @@ export const getRuntimeFieldEditorLoader =
(coreSetup: CoreSetup) => async (): Promise<LoadEditorResponse> => {
const { RuntimeFieldEditorFlyoutContent } = await import('./components');
const [core] = await coreSetup.getStartServices();
const { uiSettings, overlays, docLinks } = core;
const { uiSettings, theme, overlays, docLinks } = core;
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings });
let overlayRef: OverlayRef | null = null;
@ -50,7 +50,8 @@ export const getRuntimeFieldEditorLoader =
defaultValue={defaultValue}
ctx={ctx}
/>
</KibanaReactContextProvider>
</KibanaReactContextProvider>,
{ theme$: theme.theme$ }
)
);

View file

@ -6,7 +6,7 @@
*/
import { CoreSetup } from 'src/core/public';
import { coreMock } from 'src/core/public/mocks';
import { coreMock, themeServiceMock } from 'src/core/public/mocks';
jest.mock('../../../../src/plugins/kibana_react/public', () => {
const original = jest.requireActual('../../../../src/plugins/kibana_react/public');
@ -52,6 +52,7 @@ describe('RuntimeFieldsPlugin', () => {
openFlyout,
},
uiSettings: {},
theme: themeServiceMock.createStartContract(),
};
coreSetup.getStartServices = async () => [mockCore] as any;
const setupApi = await plugin.setup(coreSetup, {});