diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a34c5c88..b09a7c01f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added - Support for Wazuh 4.9.0 +- Added AngularJS dependencies [#6145](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6145) ## Wazuh v4.8.1 - OpenSearch Dashboards 2.10.0 - Revision 00 diff --git a/docker/osd-dev/dev.sh b/docker/osd-dev/dev.sh index 19558e9518..6e56b90aa0 100755 --- a/docker/osd-dev/dev.sh +++ b/docker/osd-dev/dev.sh @@ -12,6 +12,7 @@ os_versions=( '2.8.0' '2.9.0' '2.10.0' + '2.11.0' ) osd_versions=( @@ -26,6 +27,7 @@ osd_versions=( '2.8.0' '2.9.0' '2.10.0' + '2.11.0' '4.6.0' '4.7.0' ) diff --git a/plugins/main/package.json b/plugins/main/package.json index 81563935b9..0641b43211 100644 --- a/plugins/main/package.json +++ b/plugins/main/package.json @@ -3,7 +3,7 @@ "version": "4.9.0", "revision": "00", "pluginPlatform": { - "version": "2.10.0" + "version": "2.11.0" }, "description": "Wazuh dashboard", "keywords": [ @@ -44,8 +44,10 @@ "prebuild": "node scripts/generate-build-version" }, "dependencies": { + "angular": "^1.8.2", "angular-animate": "1.8.3", "angular-material": "1.2.5", + "angular-sanitize": "^1.8.0", "axios": "^1.6.1", "install": "^0.13.0", "js2xmlparser": "^5.0.0", @@ -69,6 +71,10 @@ "@types/node-cron": "^2.0.3", "@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/parser": "^6.2.1", + "angular-aria": "^1.8.0", + "angular-route": "^1.8.0", + "angular-mocks": "^1.8.2", + "ngreact": "^0.5.1", "eslint": "^8.46.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "3.5.5", diff --git a/plugins/main/public/components/common/modules/discover/discover.tsx b/plugins/main/public/components/common/modules/discover/discover.tsx index 130d33ec82..af57b12b9a 100644 --- a/plugins/main/public/components/common/modules/discover/discover.tsx +++ b/plugins/main/public/components/common/modules/discover/discover.tsx @@ -11,7 +11,10 @@ */ import React, { Component } from 'react'; import './discover.scss'; -import { FilterManager, Filter } from '../../../../../../../src/plugins/data/public/'; +import { + FilterManager, + Filter, +} from '../../../../../../../src/plugins/data/public/'; import { GenericRequest } from '../../../../react-services/generic-request'; import { AppState } from '../../../../react-services/app-state'; import { AppNavigate } from '../../../../react-services/app-navigate'; @@ -52,9 +55,13 @@ import { buildOpenSearchQuery, IFieldType, } from '../../../../../../../src/plugins/data/common'; -import { getDataPlugin, getToasts, getUiSettings } from '../../../../kibana-services'; +import { + getDataPlugin, + getToasts, + getUiSettings, +} from '../../../../kibana-services'; -const mapStateToProps = (state) => ({ +const mapStateToProps = state => ({ currentAgentData: state.appStateReducers.currentAgentData, }); @@ -66,7 +73,7 @@ interface ColumnDefinition { export const Discover = compose( withErrorBoundary, withReduxProvider, - connect(mapStateToProps) + connect(mapStateToProps), )( class Discover extends Component { _isMount!: boolean; @@ -156,12 +163,14 @@ export const Discover = compose( async componentDidMount() { this._isMount = true; try { - this.timeSubscription = this.timefilter.getTimeUpdate$().subscribe(() => { - this.setState({ - dateRange: this.timefilter.getTime(), - dateRangeHistory: this.timefilter._history, + this.timeSubscription = this.timefilter + .getTimeUpdate$() + .subscribe(() => { + this.setState({ + dateRange: this.timefilter.getTime(), + dateRangeHistory: this.timefilter._history, + }); }); - }); this.setState({ columns: this.getColumns() }); //initial columns await this.getIndexPattern(); await this.getAlerts(); @@ -208,7 +217,9 @@ export const Discover = compose( this.props.refreshAngularDiscover !== prevProps.refreshAngularDiscover ) { this.setState({ pageIndex: 0, tsUpdated: Date.now() }); - if (!_.isEqual(this.props.shareFilterManager, this.state.searchBarFilters)) { + if ( + !_.isEqual(this.props.shareFilterManager, this.state.searchBarFilters) + ) { this.setState({ columns: this.getColumns(), searchBarFilters: this.props.shareFilterManager || [], @@ -218,7 +229,7 @@ export const Discover = compose( } if ( ['pageIndex', 'pageSize', 'sortField', 'sortDirection'].some( - (field) => this.state[field] !== prevState[field] + field => this.state[field] !== prevState[field], ) || this.state.tsUpdated !== prevState.tsUpdated ) { @@ -249,10 +260,12 @@ export const Discover = compose( } getColumns() { //Extract array of terms from object - return this.getInnitialDefinitions().map((column) => column.field); + return this.getInnitialDefinitions().map(column => column.field); } getLabel(field) { - const innitialLabels = this.getInnitialDefinitions().filter((value) => value.field === field); + const innitialLabels = this.getInnitialDefinitions().filter( + value => value.field === field, + ); if (innitialLabels.length) { return innitialLabels[0].label || field; } else { @@ -262,18 +275,22 @@ export const Discover = compose( async getIndexPattern() { this.indexPattern = { - ...(await this.PluginPlatformServices.indexPatterns.get(AppState.getCurrentPattern())), + ...(await this.PluginPlatformServices.indexPatterns.get( + AppState.getCurrentPattern(), + )), }; } hideCreateCustomLabel = () => { try { const button = document.querySelector( - '.wz-discover #addFilterPopover > div > button > span > span' + '.wz-discover #addFilterPopover > div > button > span > span', ); if (!button) return setTimeout(this.hideCreateCustomLabel, 100); const findAndHide = () => { - const switcher = document.querySelector('#filterEditorCustomLabelSwitch'); + const switcher = document.querySelector( + '#filterEditorCustomLabelSwitch', + ); if (!switcher) return setTimeout(findAndHide, 100); switcher.parentElement.style.display = 'none'; }; @@ -304,7 +321,7 @@ export const Discover = compose( return result; } - toggleDetails = (item) => { + toggleDetails = item => { const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; const { rowDetailsFields } = this.props; @@ -318,9 +335,9 @@ export const Discover = compose( {' '} this.addFilter(filter)} - addFilterOut={(filter) => this.addFilterOut(filter)} - toggleColumn={(id) => this.addColumn(id)} + addFilter={filter => this.addFilter(filter)} + addFilterOut={filter => this.addFilterOut(filter)} + toggleColumn={id => this.addColumn(id)} rowDetailsFields={rowDetailsFields} indexPattern={this.indexPattern} /> @@ -331,8 +348,10 @@ export const Discover = compose( }; buildFilter() { - const dateParse = (ds) => - /\d+-\d+-\d+T\d+:\d+:\d+.\d+Z/.test(ds) ? DateMatch.parse(ds).toDate().getTime() : ds; + const dateParse = ds => + /\d+-\d+-\d+T\d+:\d+:\d+.\d+Z/.test(ds) + ? DateMatch.parse(ds).toDate().getTime() + : ds; const { query } = this.state; const { hideManagerAlerts } = this.wazuhConfig.getConfig(); const extraFilters = []; @@ -355,7 +374,9 @@ export const Discover = compose( ? this.props.shareFilterManager.getFilters() : []; const previousFilters = - (this.PluginPlatformServices && this.PluginPlatformServices.query.filterManager.getFilters()) || []; + (this.PluginPlatformServices && + this.PluginPlatformServices.query.filterManager.getFilters()) || + []; const elasticQuery = buildOpenSearchQuery( this.indexPattern, query, @@ -363,9 +384,9 @@ export const Discover = compose( previousFilters, filters, extraFilters, - this.props.shareFilterManagerWithUserAuthorized || [] + this.props.shareFilterManagerWithUserAuthorized || [], ), - getOpenSearchQueryConfig(getUiSettings()) + getOpenSearchQueryConfig(getUiSettings()), ); const { sortField, sortDirection } = this.state; @@ -382,10 +403,10 @@ export const Discover = compose( elasticQuery.bool.must.push(range); if (this.props.implicitFilters) { - this.props.implicitFilters.map((impicitFilter) => + this.props.implicitFilters.map(impicitFilter => elasticQuery.bool.must.push({ match: impicitFilter, - }) + }), ); } if (this.props.currentAgentData.id) { @@ -397,7 +418,9 @@ export const Discover = compose( query: elasticQuery, size: this.state.pageSize, from: this.state.pageIndex * this.state.pageSize, - ...(sortField ? { sort: { [sortField]: { order: sortDirection } } } : {}), + ...(sortField + ? { sort: { [sortField]: { order: sortDirection } } } + : {}), }; } @@ -435,8 +458,8 @@ export const Discover = compose( } const columns = this.state.columns; columns.splice( - columns.findIndex((v) => v === id), - 1 + columns.findIndex(v => v === id), + 1, ); this.setState(columns); } @@ -446,7 +469,7 @@ export const Discover = compose( this.showToast('warning', 'The maximum number of columns is 10', 3000); return; } - if (this.state.columns.find((element) => element === id)) { + if (this.state.columns.find(element => element === id)) { this.removeColumn(id); return; } @@ -457,16 +480,20 @@ export const Discover = compose( columns = () => { var columnsList = [...this.state.columns]; - const columns = columnsList.map((item) => { + const columns = columnsList.map(item => { if (item === 'icon') { return { width: '2.3%', isExpander: true, - render: (item) => { + render: item => { return ( ); }, @@ -478,7 +505,7 @@ export const Discover = compose( name: 'Time', width: '10%', sortable: true, - render: (time) => { + render: time => { return {formatUIDate(time)}; }, }; @@ -495,7 +522,10 @@ export const Discover = compose( if (item === 'agent.id') { link = (ev, x) => { - AppNavigate.navigateToModule(ev, 'agents', { tab: 'welcome', agent: x }); + AppNavigate.navigateToModule(ev, 'agents', { + tab: 'welcome', + agent: x, + }); }; width = '8%'; } @@ -507,10 +537,16 @@ export const Discover = compose( } if (item === 'rule.id') { link = (ev, x) => - AppNavigate.navigateToModule(ev, 'manager', { tab: 'rules', redirectRule: x }); + AppNavigate.navigateToModule(ev, 'manager', { + tab: 'rules', + redirectRule: x, + }); width = '9%'; } - if (item === 'rule.description' && columnsList.indexOf('syscheck.event') === -1) { + if ( + item === 'rule.description' && + columnsList.indexOf('syscheck.event') === -1 + ) { width = '30%'; } if (item === 'syscheck.event') { @@ -537,16 +573,20 @@ export const Discover = compose( > {this.getLabel(item)}{' '} {this.state.hover === item && ( - + { + style={{ + paddingBottom: 12, + marginBottom: '-10px', + paddingTop: 0, + }} + onClick={e => { this.removeColumn(item); e.stopPropagation(); }} - iconType="cross" - aria-label="Filter" - iconSize="s" + iconType='cross' + aria-label='Filter' + iconSize='s' /> )} @@ -562,20 +602,23 @@ export const Discover = compose( (link && item !== 'rule.mitre.id') || (item === 'rule.mitre.id' && this.props.shareFilterManager) ) { - column.render = (itemValue) => { + column.render = itemValue => { return ( {(item === 'agent.id' && itemValue === '000' && ( - {itemValue} + + {itemValue} + )) || (item === 'rule.mitre.id' && Array.isArray(itemValue) && - itemValue.map((currentItem) => ( + itemValue.map((currentItem, key) => ( { + key={key} + onClick={ev => { ev.stopPropagation(); }} - onMouseDown={(ev) => { + onMouseDown={ev => { ev.stopPropagation(); link(ev, currentItem); }} @@ -584,10 +627,10 @@ export const Discover = compose( ))) || ( { + onClick={ev => { ev.stopPropagation(); }} - onMouseDown={(ev) => { + onMouseDown={ev => { ev.stopPropagation(); link(ev, itemValue); }} @@ -634,11 +677,11 @@ export const Discover = compose( const key = Object.keys(filter)[0]; const value = filter[key]; const valuesArray = Array.isArray(value) ? [...value] : [value]; - valuesArray.map((item) => { + valuesArray.map(item => { const formattedFilter = buildPhraseFilter( { name: key, type: 'string' }, item, - this.indexPattern + this.indexPattern, ); formattedFilter.meta.negate = true; @@ -656,11 +699,11 @@ export const Discover = compose( const key = Object.keys(filter)[0]; const value = filter[key]; const valuesArray = Array.isArray(value) ? [...value] : [value]; - valuesArray.map((item) => { + valuesArray.map(item => { const formattedFilter = buildPhraseFilter( { name: key, type: 'string' }, item, - this.indexPattern + this.indexPattern, ); if ( formattedFilter.meta.key === 'manager.name' || @@ -673,7 +716,10 @@ export const Discover = compose( this.setState({ pageIndex: 0, tsUpdated: Date.now() }); } - onQuerySubmit = (payload: { dateRange: TimeRange; query: Query | undefined }) => { + onQuerySubmit = (payload: { + dateRange: TimeRange; + query: Query | undefined; + }) => { this.setState({ ...payload, tsUpdated: Date.now() }); }; @@ -681,7 +727,6 @@ export const Discover = compose( this.setState({ pageIndex: 0, tsUpdated: Date.now() }); }; - openDiscover(e, techniqueID) { AppNavigate.navigateToModule(e, 'overview', { tab: 'mitre', @@ -699,7 +744,12 @@ export const Discover = compose( } openIntelligence(e, redirectTo, itemID) { - AppNavigate.navigateToModule(e, 'overview', { "tab": 'mitre', "tabView": "intelligence", "tabRedirect": redirectTo, "idToRedirect": itemID }); + AppNavigate.navigateToModule(e, 'overview', { + tab: 'mitre', + tabView: 'intelligence', + tabRedirect: redirectTo, + idToRedirect: itemID, + }); } render() { @@ -711,7 +761,7 @@ export const Discover = compose( ); const { total, itemIdToExpandedRowMap } = this.state; const { query = this.state.query } = this.props; - const getRowProps = (item) => { + const getRowProps = item => { const { _id } = item; return { 'data-test-subj': `row-${_id}`, @@ -737,7 +787,7 @@ export const Discover = compose( }; const noResultsText = `No results match for this search criteria`; return ( -
+
{this.props.kbnSearchBar && ( this.setState({ dateRange }), + setTimeFilter: dateRange => this.setState({ dateRange }), }} onQuerySubmit={this.onQuerySubmit} onFiltersUpdated={this.onFiltersUpdated} @@ -757,9 +807,12 @@ export const Discover = compose( {this.state.alerts.length && ( ({ ...alert._source, _id: alert._id }))} - className="module-discover-table" - itemId="_id" + items={this.state.alerts.map(alert => ({ + ...alert._source, + _id: alert._id, + }))} + className='module-discover-table' + itemId='_id' itemIdToExpandedRowMap={itemIdToExpandedRowMap} isExpandable={true} columns={columns} @@ -774,13 +827,17 @@ export const Discover = compose( ) : ( - - + + )}
); } - } + }, ); diff --git a/plugins/main/public/get_inner_angular.ts b/plugins/main/public/get_inner_angular.ts index c186f65f1d..637f2a1b32 100644 --- a/plugins/main/public/get_inner_angular.ts +++ b/plugins/main/public/get_inner_angular.ts @@ -23,8 +23,16 @@ import angular from 'angular'; // required for `ngSanitize` angular module import 'angular-sanitize'; -import { i18nDirective, i18nFilter, I18nProvider } from '@osd/i18n/angular'; -import { CoreStart, PluginInitializerContext } from 'opensearch_dashboards/public'; +import 'ngreact'; +import { + i18nDirective, + i18nFilter, + I18nProvider, +} from './kibana-integrations/packages/osd-i18n/angular'; +import { + CoreStart, + PluginInitializerContext, +} from 'opensearch_dashboards/public'; import { Storage } from '../../../src/plugins/opensearch_dashboards_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../src/plugins/navigation/public'; import { @@ -36,7 +44,7 @@ import { watchMultiDecorator, createTopNavDirective, createTopNavHelper, -} from '../../../src/plugins/opensearch_dashboards_legacy/public'; +} from './kibana-integrations/plugins/opensearch_dashboards_legacy/public'; import { AppPluginStartDependencies } from './types'; import { getScopedHistory, setDiscoverModule } from './kibana-services'; import { createDiscoverLegacyDirective } from './kibana-integrations/discover/application/components/create_discover_legacy_directive'; @@ -51,17 +59,25 @@ export function getInnerAngularModule( name: string, core: CoreStart, deps: AppPluginStartDependencies, - context: PluginInitializerContext + context: PluginInitializerContext, ) { initAngularBootstrap(); const module = initializeInnerAngularModule(name, deps.navigation); - configureAppAngularModule(module, { core, env: context.env }, true, getScopedHistory); + configureAppAngularModule( + module, + { core, env: context.env }, + true, + getScopedHistory, + ); return module; } let initialized = false; -export function initializeInnerAngularModule(name = 'app/wazuh', navigation: NavigationStart) { +export function initializeInnerAngularModule( + name = 'app/wazuh', + navigation: NavigationStart, +) { if (!initialized) { createLocalI18nModule(); createLocalPrivateModule(); @@ -93,12 +109,14 @@ export function initializeInnerAngularModule(name = 'app/wazuh', navigation: Nav .config(watchMultiDecorator) .run(registerListenEventListener) .directive('discoverLegacy', createDiscoverLegacyDirective) - .directive('icon', (reactDirective) => reactDirective(EuiIcon)) + .directive('icon', reactDirective => reactDirective(EuiIcon)) .directive('contextErrorMessage', createContextErrorMessageDirective); } function createLocalPromiseModule() { - angular.module('discoverPromise', []).service('Promise', PromiseServiceCreator); + angular + .module('discoverPromise', []) + .service('Promise', PromiseServiceCreator); } function createLocalPrivateModule() { diff --git a/plugins/main/public/kibana-integrations/kibana-discover.js b/plugins/main/public/kibana-integrations/kibana-discover.js index b0c5404fff..e571f705d3 100644 --- a/plugins/main/public/kibana-integrations/kibana-discover.js +++ b/plugins/main/public/kibana-integrations/kibana-discover.js @@ -32,12 +32,10 @@ import { getServices, setServices, setDocViewsRegistry, - subscribeWithScope, tabifyAggResponse, - getHeaderActionMenuMounter, setUiActions, } from './discover/kibana_services'; - +import { subscribeWithScope } from './plugins/opensearch_dashboards_legacy/public'; import indexTemplateLegacy from './discover/application/angular/discover_legacy.html'; getAngularModule().directive('kbnDis', [ diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/directive.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/directive.ts new file mode 100644 index 0000000000..790357da7d --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/directive.ts @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IDirective, IRootElementService, IScope } from 'angular'; + +import { I18nServiceType } from './provider'; + +interface I18nScope extends IScope { + values?: Record; + defaultMessage: string; + id: string; +} + +const HTML_KEY_PREFIX = 'html_'; +const PLACEHOLDER_SEPARATOR = '@I18N@'; + +export const i18nDirective: [string, string, typeof i18nDirectiveFn] = [ + 'i18n', + '$sanitize', + i18nDirectiveFn, +]; + +function i18nDirectiveFn( + i18n: I18nServiceType, + $sanitize: (html: string) => string +): IDirective { + return { + restrict: 'A', + scope: { + id: '@i18nId', + defaultMessage: '@i18nDefaultMessage', + values: ' { + setContent($element, $scope, $sanitize, i18n); + }); + } else { + setContent($element, $scope, $sanitize, i18n); + } + }, + }; +} + +function setContent( + $element: IRootElementService, + $scope: I18nScope, + $sanitize: (html: string) => string, + i18n: I18nServiceType +) { + const originalValues = $scope.values; + const valuesWithPlaceholders = {} as Record; + let hasValuesWithPlaceholders = false; + + // If we have values with the keys that start with HTML_KEY_PREFIX we should replace + // them with special placeholders that later on will be inserted as HTML + // into the DOM, the rest of the content will be treated as text. We don't + // sanitize values at this stage as some of the values can be excluded from + // the translated string (e.g. not used by ICU conditional statements). + if (originalValues) { + for (const [key, value] of Object.entries(originalValues)) { + if (key.startsWith(HTML_KEY_PREFIX)) { + valuesWithPlaceholders[ + key.slice(HTML_KEY_PREFIX.length) + ] = `${PLACEHOLDER_SEPARATOR}${key}${PLACEHOLDER_SEPARATOR}`; + + hasValuesWithPlaceholders = true; + } else { + valuesWithPlaceholders[key] = value; + } + } + } + + const label = i18n($scope.id, { + values: valuesWithPlaceholders, + defaultMessage: $scope.defaultMessage, + }); + + // If there are no placeholders to replace treat everything as text, otherwise + // insert label piece by piece replacing every placeholder with corresponding + // sanitized HTML content. + if (!hasValuesWithPlaceholders) { + $element.text(label); + } else { + $element.empty(); + for (const contentOrPlaceholder of label.split(PLACEHOLDER_SEPARATOR)) { + if (!contentOrPlaceholder) { + continue; + } + + $element.append( + originalValues!.hasOwnProperty(contentOrPlaceholder) + ? $sanitize(originalValues![contentOrPlaceholder]) + : document.createTextNode(contentOrPlaceholder) + ); + } + } +} diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/filter.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/filter.ts new file mode 100644 index 0000000000..4ffa5dd3ef --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/filter.ts @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { I18nServiceType } from './provider'; + +export const i18nFilter: [string, typeof i18nFilterFn] = ['i18n', i18nFilterFn]; + +function i18nFilterFn(i18n: I18nServiceType) { + return (id: string, { defaultMessage = '', values = {} } = {}) => { + return i18n(id, { + values, + defaultMessage, + }); + }; +} diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/index.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/index.ts new file mode 100644 index 0000000000..04f7d66eb1 --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/index.ts @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { I18nProvider } from './provider'; + +export { i18nFilter } from './filter'; +export { i18nDirective } from './directive'; + +// re-export types: https://github.com/babel/babel-loader/issues/603 +import { I18nServiceType as _I18nServiceType } from './provider'; +export type I18nServiceType = _I18nServiceType; diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/provider.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/provider.ts new file mode 100644 index 0000000000..bd02a30cef --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/angular/provider.ts @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as i18n from '../core'; + +export type I18nServiceType = ReturnType; + +export class I18nProvider implements angular.IServiceProvider { + public addTranslation = i18n.addTranslation; + public getTranslation = i18n.getTranslation; + public setLocale = i18n.setLocale; + public getLocale = i18n.getLocale; + public setDefaultLocale = i18n.setDefaultLocale; + public getDefaultLocale = i18n.getDefaultLocale; + public setFormats = i18n.setFormats; + public getFormats = i18n.getFormats; + public getRegisteredLocales = i18n.getRegisteredLocales; + public init = i18n.init; + public load = i18n.load; + public $get = () => i18n.translate; +} diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/core/formats.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/formats.ts new file mode 100644 index 0000000000..f87fd57e6c --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/formats.ts @@ -0,0 +1,163 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Default format options used for "en" locale. + * These are used when constructing the internal Intl.NumberFormat + * (`number` formatter) and Intl.DateTimeFormat (`date` and `time` formatters) instances. + * The value of each parameter of `number` formatter is options object which is + * described in `options` section of [NumberFormat constructor]. + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat} + * The value of each parameter of `date` and `time` formatters is options object which is + * described in `options` section of [DateTimeFormat constructor]. + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat} + */ +export const formats: Formats = { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + relative: { + years: { + units: 'year', + }, + months: { + units: 'month', + }, + days: { + units: 'day', + }, + hours: { + units: 'hour', + }, + minutes: { + units: 'minute', + }, + seconds: { + units: 'second', + }, + }, +}; + +interface NumberFormatOptions extends Intl.NumberFormatOptions { + style?: TStyle; + localeMatcher?: 'lookup' | 'best fit'; + currencyDisplay?: 'symbol' | 'code' | 'name'; +} + +export interface Formats { + number?: Partial<{ + [key: string]: NumberFormatOptions<'currency' | 'percent' | 'decimal'>; + currency: NumberFormatOptions<'currency'>; + percent: NumberFormatOptions<'percent'>; + }>; + date?: Partial<{ + [key: string]: DateTimeFormatOptions; + short: DateTimeFormatOptions; + medium: DateTimeFormatOptions; + long: DateTimeFormatOptions; + full: DateTimeFormatOptions; + }>; + time?: Partial<{ + [key: string]: DateTimeFormatOptions; + short: DateTimeFormatOptions; + medium: DateTimeFormatOptions; + long: DateTimeFormatOptions; + full: DateTimeFormatOptions; + }>; + relative?: Partial<{ + [key: string]: { + style?: 'numeric' | 'best fit'; + units: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'; + }; + }>; +} + +interface DateTimeFormatOptions extends Intl.DateTimeFormatOptions { + weekday?: 'narrow' | 'short' | 'long'; + era?: 'narrow' | 'short' | 'long'; + year?: 'numeric' | '2-digit'; + month?: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long'; + day?: 'numeric' | '2-digit'; + hour?: 'numeric' | '2-digit'; + minute?: 'numeric' | '2-digit'; + second?: 'numeric' | '2-digit'; + timeZoneName?: 'short' | 'long'; +} diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/core/helper.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/helper.ts new file mode 100644 index 0000000000..528465e539 --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/helper.ts @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const isString = (value: any): value is string => typeof value === 'string'; + +export const isObject = (value: any): value is object => + typeof value === 'object' && value !== null; + +export const hasValues = (values: any) => Object.keys(values).length > 0; + +export const unique = (arr: T[] = []): T[] => [...new Set(arr)]; + +const merge = (a: any, b: any): { [k: string]: any } => + unique([...Object.keys(a), ...Object.keys(b)]).reduce((acc, key) => { + if (isObject(a[key]) && isObject(b[key]) && !Array.isArray(a[key]) && !Array.isArray(b[key])) { + return { + ...acc, + [key]: merge(a[key], b[key]), + }; + } + + return { + ...acc, + [key]: b[key] === undefined ? a[key] : b[key], + }; + }, {}); + +export const mergeAll = (...sources: any[]) => + sources.filter(isObject).reduce((acc, source) => merge(acc, source)); diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/core/i18n.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/i18n.ts new file mode 100644 index 0000000000..3268fae507 --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/i18n.ts @@ -0,0 +1,265 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import memoizeIntlConstructor from 'intl-format-cache'; +import IntlMessageFormat from 'intl-messageformat'; +import IntlRelativeFormat from 'intl-relativeformat'; + +import { Translation } from '../translation'; +import { Formats, formats as EN_FORMATS } from './formats'; +import { hasValues, isObject, isString, mergeAll } from './helper'; +import { isPseudoLocale, translateUsingPseudoLocale } from './pseudo_locale'; + +// Add all locale data to `IntlMessageFormat`. +import './locales.js'; + +const EN_LOCALE = 'en'; +const translationsForLocale: Record = {}; +const getMessageFormat = memoizeIntlConstructor(IntlMessageFormat); + +let defaultLocale = EN_LOCALE; +let currentLocale = EN_LOCALE; +let formats = EN_FORMATS; + +IntlMessageFormat.defaultLocale = defaultLocale; +IntlRelativeFormat.defaultLocale = defaultLocale; + +/** + * Returns message by the given message id. + * @param id - path to the message + */ +function getMessageById(id: string): string | undefined { + const translation = getTranslation(); + return translation.messages ? translation.messages[id] : undefined; +} + +/** + * Normalizes locale to make it consistent with IntlMessageFormat locales + * @param locale + */ +function normalizeLocale(locale: string) { + return locale.toLowerCase(); +} + +/** + * Provides a way to register translations with the engine + * @param newTranslation + * @param [locale = messages.locale] + */ +export function addTranslation(newTranslation: Translation, locale = newTranslation.locale) { + if (!locale || !isString(locale)) { + throw new Error('[I18n] A `locale` must be a non-empty string to add messages.'); + } + + if (newTranslation.locale && newTranslation.locale !== locale) { + throw new Error( + '[I18n] A `locale` in the translation object is different from the one provided as a second argument.' + ); + } + + const normalizedLocale = normalizeLocale(locale); + const existingTranslation = translationsForLocale[normalizedLocale] || { messages: {} }; + + translationsForLocale[normalizedLocale] = { + formats: newTranslation.formats || existingTranslation.formats, + locale: newTranslation.locale || existingTranslation.locale, + messages: { + ...existingTranslation.messages, + ...newTranslation.messages, + }, + }; +} + +/** + * Returns messages for the current language + */ +export function getTranslation(): Translation { + return translationsForLocale[currentLocale] || { messages: {} }; +} + +/** + * Tells the engine which language to use by given language key + * @param locale + */ +export function setLocale(locale: string) { + if (!locale || !isString(locale)) { + throw new Error('[I18n] A `locale` must be a non-empty string.'); + } + + currentLocale = normalizeLocale(locale); +} + +/** + * Returns the current locale + */ +export function getLocale() { + return currentLocale; +} + +/** + * Tells the library which language to fallback when missing translations + * @param locale + */ +export function setDefaultLocale(locale: string) { + if (!locale || !isString(locale)) { + throw new Error('[I18n] A `locale` must be a non-empty string.'); + } + + defaultLocale = normalizeLocale(locale); + IntlMessageFormat.defaultLocale = defaultLocale; + IntlRelativeFormat.defaultLocale = defaultLocale; +} + +export function getDefaultLocale() { + return defaultLocale; +} + +/** + * Supplies a set of options to the underlying formatter + * [Default format options used as the prototype of the formats] + * {@link https://github.com/yahoo/intl-messageformat/blob/master/src/core.js#L62} + * These are used when constructing the internal Intl.NumberFormat + * and Intl.DateTimeFormat instances. + * @param newFormats + * @param [newFormats.number] + * @param [newFormats.date] + * @param [newFormats.time] + */ +export function setFormats(newFormats: Formats) { + if (!isObject(newFormats) || !hasValues(newFormats)) { + throw new Error('[I18n] A `formats` must be a non-empty object.'); + } + + formats = mergeAll(formats, newFormats); +} + +/** + * Returns current formats + */ +export function getFormats() { + return formats; +} + +/** + * Returns array of locales having translations + */ +export function getRegisteredLocales() { + return Object.keys(translationsForLocale); +} + +interface TranslateArguments { + values?: Record; + defaultMessage: string; + description?: string; +} + +/** + * Translate message by id + * @param id - translation id to be translated + * @param [options] + * @param [options.values] - values to pass into translation + * @param [options.defaultMessage] - will be used unless translation was successful + */ +export function translate(id: string, { values = {}, defaultMessage }: TranslateArguments) { + const shouldUsePseudoLocale = isPseudoLocale(currentLocale); + + if (!id || !isString(id)) { + throw new Error('[I18n] An `id` must be a non-empty string to translate a message.'); + } + + const message = shouldUsePseudoLocale ? defaultMessage : getMessageById(id); + + if (!message && !defaultMessage) { + throw new Error(`[I18n] Cannot format message: "${id}". Default message must be provided.`); + } + + if (message) { + try { + // We should call `format` even for messages without any value references + // to let it handle escaped curly braces `\\{` that are the part of the text itself + // and not value reference boundaries. + const formattedMessage = getMessageFormat(message, getLocale(), getFormats()).format(values); + + return shouldUsePseudoLocale + ? translateUsingPseudoLocale(formattedMessage) + : formattedMessage; + } catch (e) { + throw new Error( + `[I18n] Error formatting message: "${id}" for locale: "${getLocale()}".\n${e}` + ); + } + } + + try { + const msg = getMessageFormat(defaultMessage, getDefaultLocale(), getFormats()); + + return msg.format(values); + } catch (e) { + throw new Error(`[I18n] Error formatting the default message for: "${id}".\n${e}`); + } +} + +/** + * Initializes the engine + * @param newTranslation + */ +export function init(newTranslation?: Translation) { + if (!newTranslation) { + return; + } + + addTranslation(newTranslation); + + if (newTranslation.locale) { + setLocale(newTranslation.locale); + } + + if (newTranslation.formats) { + setFormats(newTranslation.formats); + } +} + +/** + * Loads JSON with translations from the specified URL and initializes i18n engine with them. + * @param translationsUrl URL pointing to the JSON bundle with translations. + */ +export async function load(translationsUrl: string) { + // Once this package is integrated into core OpenSearch Dashboards we should switch to an abstraction + // around `fetch` provided by the platform, e.g. `kfetch`. + const response = await fetch(translationsUrl, { + credentials: 'same-origin', + }); + + if (response.status >= 300) { + throw new Error(`Translations request failed with status code: ${response.status}`); + } + + init(await response.json()); +} diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/core/index.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/index.ts new file mode 100644 index 0000000000..fe0619fa1e --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/index.ts @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { formats } from './formats'; +export * from './i18n'; diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/core/locales.js b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/locales.js new file mode 100644 index 0000000000..a837462b74 --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/locales.js @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* eslint-disable */ + +// Copied from https://github.com/yahoo/intl-relativeformat/tree/master/dist/locale-data + +import IntlMessageFormat from 'intl-messageformat'; +import IntlRelativeFormat from 'intl-relativeformat'; + +function addLocaleData(localeData) { + IntlMessageFormat.__addLocaleData(localeData); + IntlRelativeFormat.__addLocaleData(localeData); +} + +addLocaleData({ locale: "en", pluralRuleFunction: function (n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return n10==1&&n100!=11?"one":n10==2&&n100!=12?"two":n10==3&&n100!=13?"few":"other";return n==1&&v0?"one":"other"},"fields":{"year":{"displayName":"year","relative":{"0":"this year","1":"next year","-1":"last year"},"relativeTime":{"future":{"one":"in {0} year","other":"in {0} years"},"past":{"one":"{0} year ago","other":"{0} years ago"}}},"year-short":{"displayName":"yr.","relative":{"0":"this yr.","1":"next yr.","-1":"last yr."},"relativeTime":{"future":{"one":"in {0} yr.","other":"in {0} yr."},"past":{"one":"{0} yr. ago","other":"{0} yr. ago"}}},"month":{"displayName":"month","relative":{"0":"this month","1":"next month","-1":"last month"},"relativeTime":{"future":{"one":"in {0} month","other":"in {0} months"},"past":{"one":"{0} month ago","other":"{0} months ago"}}},"month-short":{"displayName":"mo.","relative":{"0":"this mo.","1":"next mo.","-1":"last mo."},"relativeTime":{"future":{"one":"in {0} mo.","other":"in {0} mo."},"past":{"one":"{0} mo. ago","other":"{0} mo. ago"}}},"day":{"displayName":"day","relative":{"0":"today","1":"tomorrow","-1":"yesterday"},"relativeTime":{"future":{"one":"in {0} day","other":"in {0} days"},"past":{"one":"{0} day ago","other":"{0} days ago"}}},"day-short":{"displayName":"day","relative":{"0":"today","1":"tomorrow","-1":"yesterday"},"relativeTime":{"future":{"one":"in {0} day","other":"in {0} days"},"past":{"one":"{0} day ago","other":"{0} days ago"}}},"hour":{"displayName":"hour","relative":{"0":"this hour"},"relativeTime":{"future":{"one":"in {0} hour","other":"in {0} hours"},"past":{"one":"{0} hour ago","other":"{0} hours ago"}}},"hour-short":{"displayName":"hr.","relative":{"0":"this hour"},"relativeTime":{"future":{"one":"in {0} hr.","other":"in {0} hr."},"past":{"one":"{0} hr. ago","other":"{0} hr. ago"}}},"minute":{"displayName":"minute","relative":{"0":"this minute"},"relativeTime":{"future":{"one":"in {0} minute","other":"in {0} minutes"},"past":{"one":"{0} minute ago","other":"{0} minutes ago"}}},"minute-short":{"displayName":"min.","relative":{"0":"this minute"},"relativeTime":{"future":{"one":"in {0} min.","other":"in {0} min."},"past":{"one":"{0} min. ago","other":"{0} min. ago"}}},"second":{"displayName":"second","relative":{"0":"now"},"relativeTime":{"future":{"one":"in {0} second","other":"in {0} seconds"},"past":{"one":"{0} second ago","other":"{0} seconds ago"}}},"second-short":{"displayName":"sec.","relative":{"0":"now"},"relativeTime":{"future":{"one":"in {0} sec.","other":"in {0} sec."},"past":{"one":"{0} sec. ago","other":"{0} sec. ago"}}}} }); +addLocaleData({ locale: "en-US", parentLocale: "en" }); +addLocaleData({ locale: "en-xa", pluralRuleFunction: function (n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return n10==1&&n100!=11?"one":n10==2&&n100!=12?"two":n10==3&&n100!=13?"few":"other";return n==1&&v0?"one":"other"}, "fields":{"year":{"displayName":"ýéààŕ","relative":{"0":"ţĥîîš ýééàŕ","1":"ñéẋẋţ ýééàŕ","-1":"ļàššţ ýééàŕ"},"relativeTime":{"future":{"one":"îñ {0} ýýéàŕŕ","other":"îñ {0} ýýéàŕŕš"},"past":{"one":"{0} ýéààŕ àĝĝô","other":"{0} ýéààŕš ààĝô"}}},"year-short":{"displayName":"ýŕ.","relative":{"0":"ţĥîîš ýŕŕ.","1":"ñéẋẋţ ýŕŕ.","-1":"ļàššţ ýŕŕ."},"relativeTime":{"future":{"one":"îñ {0} ýýŕ.","other":"îñ {0} ýýŕ."},"past":{"one":"{0} ýŕ. ààĝô","other":"{0} ýŕ. ààĝô"}}},"month":{"displayName":"ɱôññţĥ","relative":{"0":"ţĥîîš ɱôôñţĥĥ","1":"ñéẋẋţ ɱôôñţĥĥ","-1":"ļàššţ ɱôôñţĥĥ"},"relativeTime":{"future":{"one":"îñ {0} ɱɱôñţţĥ","other":"îñ {0} ɱɱôñţţĥš"},"past":{"one":"{0} ɱôññţĥ ààĝô","other":"{0} ɱôññţĥšš àĝôô"}}},"month-short":{"displayName":"ɱô.","relative":{"0":"ţĥîîš ɱôô.","1":"ñéẋẋţ ɱôô.","-1":"ļàššţ ɱôô."},"relativeTime":{"future":{"one":"îñ {0} ɱɱô.","other":"îñ {0} ɱɱô."},"past":{"one":"{0} ɱô. ààĝô","other":"{0} ɱô. ààĝô"}}},"day":{"displayName":"ðàýý","relative":{"0":"ţôððàý","1":"ţôɱɱôŕŕŕôŵ","-1":"ýéššţéŕŕðàýý"},"relativeTime":{"future":{"one":"îñ {0} ððàý","other":"îñ {0} ððàýšš"},"past":{"one":"{0} ðàýý àĝôô","other":"{0} ðàýýš àĝĝô"}}},"day-short":{"displayName":"ðàýý","relative":{"0":"ţôððàý","1":"ţôɱɱôŕŕŕôŵ","-1":"ýéššţéŕŕðàýý"},"relativeTime":{"future":{"one":"îñ {0} ððàý","other":"îñ {0} ððàýšš"},"past":{"one":"{0} ðàýý àĝôô","other":"{0} ðàýýš àĝĝô"}}},"hour":{"displayName":"ĥôûûŕ","relative":{"0":"ţĥîîš ĥôôûŕ"},"relativeTime":{"future":{"one":"îñ {0} ĥĥôûŕŕ","other":"îñ {0} ĥĥôûŕŕš"},"past":{"one":"{0} ĥôûûŕ àĝĝô","other":"{0} ĥôûûŕš ààĝô"}}},"hour-short":{"displayName":"ĥŕ.","relative":{"0":"ţĥîîš ĥôôûŕ"},"relativeTime":{"future":{"one":"îñ {0} ĥĥŕ.","other":"îñ {0} ĥĥŕ."},"past":{"one":"{0} ĥŕ. ààĝô","other":"{0} ĥŕ. ààĝô"}}},"minute":{"displayName":"ɱîññûţéé","relative":{"0":"ţĥîîš ɱîîñûţţé"},"relativeTime":{"future":{"one":"îñ {0} ɱɱîñûûţé","other":"îñ {0} ɱɱîñûûţéšš"},"past":{"one":"{0} ɱîññûţéé àĝôô","other":"{0} ɱîññûţééš àĝĝô"}}},"minute-short":{"displayName":"ɱîññ.","relative":{"0":"ţĥîîš ɱîîñûţţé"},"relativeTime":{"future":{"one":"îñ {0} ɱɱîñ.","other":"îñ {0} ɱɱîñ."},"past":{"one":"{0} ɱîññ. àĝôô","other":"{0} ɱîññ. àĝôô"}}},"second":{"displayName":"šéççôñðð","relative":{"0":"ñôŵŵ"},"relativeTime":{"future":{"one":"îñ {0} ššéçôôñð","other":"îñ {0} ššéçôôñðšš"},"past":{"one":"{0} šéççôñðð àĝôô","other":"{0} šéççôñððš àĝĝô"}}},"second-short":{"displayName":"šéçç.","relative":{"0":"ñôŵŵ"},"relativeTime":{"future":{"one":"îñ {0} ššéç.","other":"îñ {0} ššéç."},"past":{"one":"{0} šéçç. àĝôô","other":"{0} šéçç. àĝôô"}}}} }); +addLocaleData({ locale: "es", pluralRuleFunction: function (n,ord){if(ord)return"other";return n==1?"one":"other"},"fields":{"year":{"displayName":"año","relative":{"0":"este año","1":"el próximo año","-1":"el año pasado"},"relativeTime":{"future":{"one":"dentro de {0} año","other":"dentro de {0} años"},"past":{"one":"hace {0} año","other":"hace {0} años"}}},"year-short":{"displayName":"a","relative":{"0":"este año","1":"el próximo año","-1":"el año pasado"},"relativeTime":{"future":{"one":"dentro de {0} a","other":"dentro de {0} a"},"past":{"one":"hace {0} a","other":"hace {0} a"}}},"month":{"displayName":"mes","relative":{"0":"este mes","1":"el próximo mes","-1":"el mes pasado"},"relativeTime":{"future":{"one":"dentro de {0} mes","other":"dentro de {0} meses"},"past":{"one":"hace {0} mes","other":"hace {0} meses"}}},"month-short":{"displayName":"m","relative":{"0":"este mes","1":"el próximo mes","-1":"el mes pasado"},"relativeTime":{"future":{"one":"dentro de {0} m","other":"dentro de {0} m"},"past":{"one":"hace {0} m","other":"hace {0} m"}}},"day":{"displayName":"día","relative":{"0":"hoy","1":"mañana","2":"pasado mañana","-2":"anteayer","-1":"ayer"},"relativeTime":{"future":{"one":"dentro de {0} día","other":"dentro de {0} días"},"past":{"one":"hace {0} día","other":"hace {0} días"}}},"day-short":{"displayName":"d","relative":{"0":"hoy","1":"mañana","2":"pasado mañana","-2":"anteayer","-1":"ayer"},"relativeTime":{"future":{"one":"dentro de {0} día","other":"dentro de {0} días"},"past":{"one":"hace {0} día","other":"hace {0} días"}}},"hour":{"displayName":"hora","relative":{"0":"esta hora"},"relativeTime":{"future":{"one":"dentro de {0} hora","other":"dentro de {0} horas"},"past":{"one":"hace {0} hora","other":"hace {0} horas"}}},"hour-short":{"displayName":"h","relative":{"0":"esta hora"},"relativeTime":{"future":{"one":"dentro de {0} h","other":"dentro de {0} h"},"past":{"one":"hace {0} h","other":"hace {0} h"}}},"minute":{"displayName":"minuto","relative":{"0":"este minuto"},"relativeTime":{"future":{"one":"dentro de {0} minuto","other":"dentro de {0} minutos"},"past":{"one":"hace {0} minuto","other":"hace {0} minutos"}}},"minute-short":{"displayName":"min","relative":{"0":"este minuto"},"relativeTime":{"future":{"one":"dentro de {0} min","other":"dentro de {0} min"},"past":{"one":"hace {0} min","other":"hace {0} min"}}},"second":{"displayName":"segundo","relative":{"0":"ahora"},"relativeTime":{"future":{"one":"dentro de {0} segundo","other":"dentro de {0} segundos"},"past":{"one":"hace {0} segundo","other":"hace {0} segundos"}}},"second-short":{"displayName":"s","relative":{"0":"ahora"},"relativeTime":{"future":{"one":"dentro de {0} s","other":"dentro de {0} s"},"past":{"one":"hace {0} s","other":"hace {0} s"}}}} }); +addLocaleData({ locale: "es-LA", parentLocale: "es" }); +addLocaleData({ locale: "fr", pluralRuleFunction: function (n,ord){if(ord)return n==1?"one":"other";return n>=0&&n<2?"one":"other"},"fields":{"year":{"displayName":"année","relative":{"0":"cette année","1":"l’année prochaine","-1":"l’année dernière"},"relativeTime":{"future":{"one":"dans {0} an","other":"dans {0} ans"},"past":{"one":"il y a {0} an","other":"il y a {0} ans"}}},"year-short":{"displayName":"an","relative":{"0":"cette année","1":"l’année prochaine","-1":"l’année dernière"},"relativeTime":{"future":{"one":"dans {0} a","other":"dans {0} a"},"past":{"one":"il y a {0} a","other":"il y a {0} a"}}},"month":{"displayName":"mois","relative":{"0":"ce mois-ci","1":"le mois prochain","-1":"le mois dernier"},"relativeTime":{"future":{"one":"dans {0} mois","other":"dans {0} mois"},"past":{"one":"il y a {0} mois","other":"il y a {0} mois"}}},"month-short":{"displayName":"m.","relative":{"0":"ce mois-ci","1":"le mois prochain","-1":"le mois dernier"},"relativeTime":{"future":{"one":"dans {0} m.","other":"dans {0} m."},"past":{"one":"il y a {0} m.","other":"il y a {0} m."}}},"day":{"displayName":"jour","relative":{"0":"aujourd’hui","1":"demain","2":"après-demain","-2":"avant-hier","-1":"hier"},"relativeTime":{"future":{"one":"dans {0} jour","other":"dans {0} jours"},"past":{"one":"il y a {0} jour","other":"il y a {0} jours"}}},"day-short":{"displayName":"j","relative":{"0":"aujourd’hui","1":"demain","2":"après-demain","-2":"avant-hier","-1":"hier"},"relativeTime":{"future":{"one":"dans {0} j","other":"dans {0} j"},"past":{"one":"il y a {0} j","other":"il y a {0} j"}}},"hour":{"displayName":"heure","relative":{"0":"cette heure-ci"},"relativeTime":{"future":{"one":"dans {0} heure","other":"dans {0} heures"},"past":{"one":"il y a {0} heure","other":"il y a {0} heures"}}},"hour-short":{"displayName":"h","relative":{"0":"cette heure-ci"},"relativeTime":{"future":{"one":"dans {0} h","other":"dans {0} h"},"past":{"one":"il y a {0} h","other":"il y a {0} h"}}},"minute":{"displayName":"minute","relative":{"0":"cette minute-ci"},"relativeTime":{"future":{"one":"dans {0} minute","other":"dans {0} minutes"},"past":{"one":"il y a {0} minute","other":"il y a {0} minutes"}}},"minute-short":{"displayName":"min","relative":{"0":"cette minute-ci"},"relativeTime":{"future":{"one":"dans {0} min","other":"dans {0} min"},"past":{"one":"il y a {0} min","other":"il y a {0} min"}}},"second":{"displayName":"seconde","relative":{"0":"maintenant"},"relativeTime":{"future":{"one":"dans {0} seconde","other":"dans {0} secondes"},"past":{"one":"il y a {0} seconde","other":"il y a {0} secondes"}}},"second-short":{"displayName":"s","relative":{"0":"maintenant"},"relativeTime":{"future":{"one":"dans {0} s","other":"dans {0} s"},"past":{"one":"il y a {0} s","other":"il y a {0} s"}}}} }); +addLocaleData({ locale: "fr-FR", parentLocale: "fr" }); +addLocaleData({ locale: "de", pluralRuleFunction: function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"},"fields":{"year":{"displayName":"Jahr","relative":{"0":"dieses Jahr","1":"nächstes Jahr","-1":"letztes Jahr"},"relativeTime":{"future":{"one":"in {0} Jahr","other":"in {0} Jahren"},"past":{"one":"vor {0} Jahr","other":"vor {0} Jahren"}}},"year-short":{"displayName":"Jahr","relative":{"0":"dieses Jahr","1":"nächstes Jahr","-1":"letztes Jahr"},"relativeTime":{"future":{"one":"in {0} Jahr","other":"in {0} Jahren"},"past":{"one":"vor {0} Jahr","other":"vor {0} Jahren"}}},"month":{"displayName":"Monat","relative":{"0":"diesen Monat","1":"nächsten Monat","-1":"letzten Monat"},"relativeTime":{"future":{"one":"in {0} Monat","other":"in {0} Monaten"},"past":{"one":"vor {0} Monat","other":"vor {0} Monaten"}}},"month-short":{"displayName":"Monat","relative":{"0":"diesen Monat","1":"nächsten Monat","-1":"letzten Monat"},"relativeTime":{"future":{"one":"in {0} Monat","other":"in {0} Monaten"},"past":{"one":"vor {0} Monat","other":"vor {0} Monaten"}}},"day":{"displayName":"Tag","relative":{"0":"heute","1":"morgen","2":"übermorgen","-2":"vorgestern","-1":"gestern"},"relativeTime":{"future":{"one":"in {0} Tag","other":"in {0} Tagen"},"past":{"one":"vor {0} Tag","other":"vor {0} Tagen"}}},"day-short":{"displayName":"Tag","relative":{"0":"heute","1":"morgen","2":"übermorgen","-2":"vorgestern","-1":"gestern"},"relativeTime":{"future":{"one":"in {0} Tag","other":"in {0} Tagen"},"past":{"one":"vor {0} Tag","other":"vor {0} Tagen"}}},"hour":{"displayName":"Stunde","relative":{"0":"in dieser Stunde"},"relativeTime":{"future":{"one":"in {0} Stunde","other":"in {0} Stunden"},"past":{"one":"vor {0} Stunde","other":"vor {0} Stunden"}}},"hour-short":{"displayName":"Std.","relative":{"0":"in dieser Stunde"},"relativeTime":{"future":{"one":"in {0} Std.","other":"in {0} Std."},"past":{"one":"vor {0} Std.","other":"vor {0} Std."}}},"minute":{"displayName":"Minute","relative":{"0":"in dieser Minute"},"relativeTime":{"future":{"one":"in {0} Minute","other":"in {0} Minuten"},"past":{"one":"vor {0} Minute","other":"vor {0} Minuten"}}},"minute-short":{"displayName":"Min.","relative":{"0":"in dieser Minute"},"relativeTime":{"future":{"one":"in {0} Min.","other":"in {0} Min."},"past":{"one":"vor {0} Min.","other":"vor {0} Min."}}},"second":{"displayName":"Sekunde","relative":{"0":"jetzt"},"relativeTime":{"future":{"one":"in {0} Sekunde","other":"in {0} Sekunden"},"past":{"one":"vor {0} Sekunde","other":"vor {0} Sekunden"}}},"second-short":{"displayName":"Sek.","relative":{"0":"jetzt"},"relativeTime":{"future":{"one":"in {0} Sek.","other":"in {0} Sek."},"past":{"one":"vor {0} Sek.","other":"vor {0} Sek."}}}} }); +addLocaleData({ locale: "de-DE", parentLocale: "de" }); +addLocaleData({ locale: "ja", pluralRuleFunction: function (n,ord){if(ord)return"other";return"other"},"fields":{"year":{"displayName":"年","relative":{"0":"今年","1":"翌年","-1":"昨年"},"relativeTime":{"future":{"other":"{0} 年後"},"past":{"other":"{0} 年前"}}},"year-short":{"displayName":"年","relative":{"0":"今年","1":"翌年","-1":"昨年"},"relativeTime":{"future":{"other":"{0} 年後"},"past":{"other":"{0} 年前"}}},"month":{"displayName":"月","relative":{"0":"今月","1":"翌月","-1":"先月"},"relativeTime":{"future":{"other":"{0} か月後"},"past":{"other":"{0} か月前"}}},"month-short":{"displayName":"月","relative":{"0":"今月","1":"翌月","-1":"先月"},"relativeTime":{"future":{"other":"{0} か月後"},"past":{"other":"{0} か月前"}}},"day":{"displayName":"日","relative":{"0":"今日","1":"明日","2":"明後日","-2":"一昨日","-1":"昨日"},"relativeTime":{"future":{"other":"{0} 日後"},"past":{"other":"{0} 日前"}}},"day-short":{"displayName":"日","relative":{"0":"今日","1":"明日","2":"明後日","-2":"一昨日","-1":"昨日"},"relativeTime":{"future":{"other":"{0} 日後"},"past":{"other":"{0} 日前"}}},"hour":{"displayName":"時","relative":{"0":"1 時間以内"},"relativeTime":{"future":{"other":"{0} 時間後"},"past":{"other":"{0} 時間前"}}},"hour-short":{"displayName":"時","relative":{"0":"1 時間以内"},"relativeTime":{"future":{"other":"{0} 時間後"},"past":{"other":"{0} 時間前"}}},"minute":{"displayName":"分","relative":{"0":"1 分以内"},"relativeTime":{"future":{"other":"{0} 分後"},"past":{"other":"{0} 分前"}}},"minute-short":{"displayName":"分","relative":{"0":"1 分以内"},"relativeTime":{"future":{"other":"{0} 分後"},"past":{"other":"{0} 分前"}}},"second":{"displayName":"秒","relative":{"0":"今"},"relativeTime":{"future":{"other":"{0} 秒後"},"past":{"other":"{0} 秒前"}}},"second-short":{"displayName":"秒","relative":{"0":"今"},"relativeTime":{"future":{"other":"{0} 秒後"},"past":{"other":"{0} 秒前"}}}} }); +addLocaleData({ locale: "ja-JP", parentLocale: "ja" }); +addLocaleData({ locale: "ko", pluralRuleFunction: function (n,ord){if(ord)return"other";return"other"},"fields":{"year":{"displayName":"년","relative":{"0":"올해","1":"내년","-1":"작년"},"relativeTime":{"future":{"other":"{0}년 후"},"past":{"other":"{0}년 전"}}},"year-short":{"displayName":"년","relative":{"0":"올해","1":"내년","-1":"작년"},"relativeTime":{"future":{"other":"{0}년 후"},"past":{"other":"{0}년 전"}}},"month":{"displayName":"월","relative":{"0":"이번 달","1":"다음 달","-1":"지난달"},"relativeTime":{"future":{"other":"{0}개월 후"},"past":{"other":"{0}개월 전"}}},"month-short":{"displayName":"월","relative":{"0":"이번 달","1":"다음 달","-1":"지난달"},"relativeTime":{"future":{"other":"{0}개월 후"},"past":{"other":"{0}개월 전"}}},"day":{"displayName":"일","relative":{"0":"오늘","1":"내일","2":"모레","-2":"그저께","-1":"어제"},"relativeTime":{"future":{"other":"{0}일 후"},"past":{"other":"{0}일 전"}}},"day-short":{"displayName":"일","relative":{"0":"오늘","1":"내일","2":"모레","-2":"그저께","-1":"어제"},"relativeTime":{"future":{"other":"{0}일 후"},"past":{"other":"{0}일 전"}}},"hour":{"displayName":"시","relative":{"0":"현재 시간"},"relativeTime":{"future":{"other":"{0}시간 후"},"past":{"other":"{0}시간 전"}}},"hour-short":{"displayName":"시","relative":{"0":"현재 시간"},"relativeTime":{"future":{"other":"{0}시간 후"},"past":{"other":"{0}시간 전"}}},"minute":{"displayName":"분","relative":{"0":"현재 분"},"relativeTime":{"future":{"other":"{0}분 후"},"past":{"other":"{0}분 전"}}},"minute-short":{"displayName":"분","relative":{"0":"현재 분"},"relativeTime":{"future":{"other":"{0}분 후"},"past":{"other":"{0}분 전"}}},"second":{"displayName":"초","relative":{"0":"지금"},"relativeTime":{"future":{"other":"{0}초 후"},"past":{"other":"{0}초 전"}}},"second-short":{"displayName":"초","relative":{"0":"지금"},"relativeTime":{"future":{"other":"{0}초 후"},"past":{"other":"{0}초 전"}}}} }); +addLocaleData({ locale: "ko-KR", parentLocale: "ko" }); +addLocaleData({ locale: 'ru', pluralRuleFunction: function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],i10=i.slice(-1),i100=i.slice(-2);if(ord)return"other";return v0&&i10==1&&i100!=11?"one":v0&&i10>=2&&i10<=4&&(i100<12||i100>14)?"few":(v0&&i10==0)||(v0&&i10>=5&&i10<=9)||(v0&&i100>=11&&i100<=14)?"many":"other";},"fields":{"year":{"displayName":"год","relative":{"0":"в этом году","1":"в следующем году","-1":"в прошлом году"},"relativeTime":{"future":{"one":"через {0} год","few":"через {0} года","many":"через {0} лет","other":"через {0} года"},"past":{"one":"{0} год назад","few":"{0} года назад","many":"{0} лет назад","other":"{0} года назад"}}},"year-short":{"displayName":"г.","relative":{"0":"в этом г.","1":"в след. г.","-1":"в прошлом г."},"relativeTime":{"future":{"one":"через {0} г.","few":"через {0} г.","many":"через {0} л.","other":"через {0} г."},"past":{"one":"{0} г. назад","few":"{0} г. назад","many":"{0} л. назад","other":"{0} г. назад"}}},"month":{"displayName":"месяц","relative":{"0":"в этом месяце","1":"в следующем месяце","-1":"в прошлом месяце"},"relativeTime":{"future":{"one":"через {0} месяц","few":"через {0} месяца","many":"через {0} месяцев","other":"через {0} месяца"},"past":{"one":"{0} месяц назад","few":"{0} месяца назад","many":"{0} месяцев назад","other":"{0} месяца назад"}}},"month-short":{"displayName":"мес.","relative":{"0":"в этом мес.","1":"в следующем мес.","-1":"в прошлом мес."},"relativeTime":{"future":{"one":"через {0} мес.","few":"через {0} мес.","many":"через {0} мес.","other":"через {0} мес."},"past":{"one":"{0} мес. назад","few":"{0} мес. назад","many":"{0} мес. назад","other":"{0} мес. назад"}}},"week":{"displayName":"неделя","relativePeriod":"на неделе {0}","relative":{"0":"на этой неделе","1":"на следующей неделе","-1":"на прошлой неделе"},"relativeTime":{"future":{"one":"через {0} неделю","few":"через {0} недели","many":"через {0} недель","other":"через {0} недели"},"past":{"one":"{0} неделю назад","few":"{0} недели назад","many":"{0} недель назад","other":"{0} недели назад"}}},"week-short":{"displayName":"нед.","relativePeriod":"на нед. {0}","relative":{"0":"на этой нед.","1":"на следующей нед.","-1":"на прошлой нед."},"relativeTime":{"future":{"one":"через {0} нед.","few":"через {0} нед.","many":"через {0} нед.","other":"через {0} нед."},"past":{"one":"{0} нед. назад","few":"{0} нед. назад","many":"{0} нед. назад","other":"{0} нед. назад"}}},"day":{"displayName":"день","relative":{"0":"сегодня","1":"завтра","2":"послезавтра","-1":"вчера","-2":"позавчера"},"relativeTime":{"future":{"one":"через {0} день","few":"через {0} дня","many":"через {0} дней","other":"через {0} дня"},"past":{"one":"{0} день назад","few":"{0} дня назад","many":"{0} дней назад","other":"{0} дня назад"}}},"day-short":{"displayName":"дн.","relative":{"0":"сегодня","1":"завтра","2":"послезавтра","-1":"вчера","-2":"позавчера"},"relativeTime":{"future":{"one":"через {0} дн.","few":"через {0} дн.","many":"через {0} дн.","other":"через {0} дн."},"past":{"one":"{0} дн. назад","few":"{0} дн. назад","many":"{0} дн. назад","other":"{0} дн. назад"}}},"hour":{"displayName":"час","relative":{"0":"в этот час"},"relativeTime":{"future":{"one":"через {0} час","few":"через {0} часа","many":"через {0} часов","other":"через {0} часа"},"past":{"one":"{0} час назад","few":"{0} часа назад","many":"{0} часов назад","other":"{0} часа назад"}}},"hour-short":{"displayName":"ч","relative":{"0":"в этот час"},"relativeTime":{"future":{"one":"через {0} ч.","few":"через {0} ч.","many":"через {0} ч.","other":"через {0} ч."},"past":{"one":"{0} ч. назад","few":"{0} ч. назад","many":"{0} ч. назад","other":"{0} ч. назад"}}},"minute":{"displayName":"минута","relative":{"0":"в эту минуту"},"relativeTime":{"future":{"one":"через {0} минуту","few":"через {0} минуты","many":"через {0} минут","other":"через {0} минуты"},"past":{"one":"{0} минуту назад","few":"{0} минуты назад","many":"{0} минут назад","other":"{0} минуты назад"}}},"minute-short":{"displayName":"мин.","relative":{"0":"в эту минуту"},"relativeTime":{"future":{"one":"через {0} мин.","few":"через {0} мин.","many":"через {0} мин.","other":"через {0} мин."},"past":{"one":"{0} мин. назад","few":"{0} мин. назад","many":"{0} мин. назад","other":"{0} мин. назад"}}},"second":{"displayName":"секунда","relative":{"0":"сейчас"},"relativeTime":{"future":{"one":"через {0} секунду","few":"через {0} секунды","many":"через {0} секунд","other":"через {0} секунды"},"past":{"one":"{0} секунду назад","few":"{0} секунды назад","many":"{0} секунд назад","other":"{0} секунды назад"}}},"second-short":{"displayName":"сек.","relative":{"0":"сейчас"},"relativeTime":{"future":{"one":"через {0} сек.","few":"через {0} сек.","many":"через {0} сек.","other":"через {0} сек."},"past":{"one":"{0} сек. назад","few":"{0} сек. назад","many":"{0} сек. назад","other":"{0} сек. назад"}}}} }); +addLocaleData({ locale: "ru-RU", parentLocale: "ru" }); +addLocaleData({ locale: "zh", pluralRuleFunction: function (n,ord){if(ord)return"other";return"other"},"fields":{"year":{"displayName":"年","relative":{"0":"今年","1":"明年","-1":"去年"},"relativeTime":{"future":{"other":"{0}年后"},"past":{"other":"{0}年前"}}},"year-short":{"displayName":"年","relative":{"0":"今年","1":"明年","-1":"去年"},"relativeTime":{"future":{"other":"{0}年后"},"past":{"other":"{0}年前"}}},"month":{"displayName":"月","relative":{"0":"本月","1":"下个月","-1":"上个月"},"relativeTime":{"future":{"other":"{0}个月后"},"past":{"other":"{0}个月前"}}},"month-short":{"displayName":"月","relative":{"0":"本月","1":"下个月","-1":"上个月"},"relativeTime":{"future":{"other":"{0}个月后"},"past":{"other":"{0}个月前"}}},"day":{"displayName":"日","relative":{"0":"今天","1":"明天","2":"后天","-2":"前天","-1":"昨天"},"relativeTime":{"future":{"other":"{0}天后"},"past":{"other":"{0}天前"}}},"day-short":{"displayName":"日","relative":{"0":"今天","1":"明天","2":"后天","-2":"前天","-1":"昨天"},"relativeTime":{"future":{"other":"{0}天后"},"past":{"other":"{0}天前"}}},"hour":{"displayName":"小时","relative":{"0":"这一时间 \u002F 此时"},"relativeTime":{"future":{"other":"{0}小时后"},"past":{"other":"{0}小时前"}}},"hour-short":{"displayName":"小时","relative":{"0":"这一时间 \u002F 此时"},"relativeTime":{"future":{"other":"{0}小时后"},"past":{"other":"{0}小时前"}}},"minute":{"displayName":"分钟","relative":{"0":"此刻"},"relativeTime":{"future":{"other":"{0}分钟后"},"past":{"other":"{0}分钟前"}}},"minute-short":{"displayName":"分","relative":{"0":"此刻"},"relativeTime":{"future":{"other":"{0}分钟后"},"past":{"other":"{0}分钟前"}}},"second":{"displayName":"秒","relative":{"0":"现在"},"relativeTime":{"future":{"other":"{0}秒钟后"},"past":{"other":"{0}秒钟前"}}},"second-short":{"displayName":"秒","relative":{"0":"现在"},"relativeTime":{"future":{"other":"{0}秒后"},"past":{"other":"{0}秒前"}}}} }); +addLocaleData({ locale: "zh-CN", parentLocale: "zh" }); diff --git a/plugins/main/public/kibana-integrations/packages/osd-i18n/core/pseudo_locale.ts b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/pseudo_locale.ts new file mode 100644 index 0000000000..02affa92c4 --- /dev/null +++ b/plugins/main/public/kibana-integrations/packages/osd-i18n/core/pseudo_locale.ts @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Matches every single [A-Za-z] character, ``, `](markdown-link-address)` and `@I18N@valid_variable_name@I18N@` + */ +const CHARS_FOR_PSEUDO_LOCALIZATION_REGEX = /[A-Za-z]|(\]\([\s\S]*?\))|(<([^"<>]|("[^"]*?"))*?>)|(@I18N@\w*?@I18N@)/g; +const PSEUDO_ACCENTS_LOCALE = 'en-xa'; + +export function isPseudoLocale(locale: string) { + return locale.toLowerCase() === PSEUDO_ACCENTS_LOCALE; +} + +/** + * Replaces every latin char by pseudo char and repeats every third char twice. + */ +function replacer() { + let count = 0; + + return (match: string) => { + // if `match.length !== 1`, then `match` is html tag or markdown link address, so it should be ignored + if (match.length !== 1) { + return match; + } + + const pseudoChar = pseudoAccentCharMap[match] || match; + return ++count % 3 === 0 ? pseudoChar.repeat(2) : pseudoChar; + }; +} + +export function translateUsingPseudoLocale(message: string) { + return message.replace(CHARS_FOR_PSEUDO_LOCALIZATION_REGEX, replacer()); +} + +const pseudoAccentCharMap: Record = { + a: 'à', + b: 'ƀ', + c: 'ç', + d: 'ð', + e: 'é', + f: 'ƒ', + g: 'ĝ', + h: 'ĥ', + i: 'î', + l: 'ļ', + k: 'ķ', + j: 'ĵ', + m: 'ɱ', + n: 'ñ', + o: 'ô', + p: 'þ', + q: 'ǫ', + r: 'ŕ', + s: 'š', + t: 'ţ', + u: 'û', + v: 'ṽ', + w: 'ŵ', + x: 'ẋ', + y: 'ý', + z: 'ž', + A: 'À', + B: 'Ɓ', + C: 'Ç', + D: 'Ð', + E: 'É', + F: 'Ƒ', + G: 'Ĝ', + H: 'Ĥ', + I: 'Î', + L: 'Ļ', + K: 'Ķ', + J: 'Ĵ', + M: 'Ṁ', + N: 'Ñ', + O: 'Ô', + P: 'Þ', + Q: 'Ǫ', + R: 'Ŕ', + S: 'Š', + T: 'Ţ', + U: 'Û', + V: 'Ṽ', + W: 'Ŵ', + X: 'Ẋ', + Y: 'Ý', + Z: 'Ž', +}; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx new file mode 100644 index 0000000000..fbe36a289d --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx @@ -0,0 +1,380 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ICompileProvider, + IHttpProvider, + IHttpService, + ILocationProvider, + IModule, + IRootScopeService, +} from 'angular'; +import $ from 'jquery'; +import { set } from '@elastic/safer-lodash-set'; +import { get } from 'lodash'; +import * as Rx from 'rxjs'; +import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'opensearch-dashboards/public'; +import { History } from 'history'; + +import { CoreStart } from 'opensearch-dashboards/public'; +import { isSystemApiRequest } from '../utils'; +import { formatAngularHttpError, isAngularHttpError } from '../notify/lib'; + +export interface RouteConfiguration { + controller?: string | ((...args: any[]) => void); + redirectTo?: string; + resolveRedirectTo?: (...args: any[]) => void; + reloadOnSearch?: boolean; + reloadOnUrl?: boolean; + outerAngularWrapperRoute?: boolean; + resolve?: object; + template?: string; + k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; + requireUICapability?: string; +} + +/** + * Detects whether a given angular route is a dummy route that doesn't + * require any action. There are two ways this can happen: + * If `outerAngularWrapperRoute` is set on the route config object, + * it means the local application service set up this route on the outer angular + * and the internal routes will handle the hooks. + * + * If angular did not detect a route and it is the local angular, we are currently + * navigating away from a URL controlled by a local angular router and the + * application will get unmounted. In this case the outer router will handle + * the hooks. + * @param $route Injected $route dependency + * @param isLocalAngular Flag whether this is the local angular router + */ +function isDummyRoute($route: any, isLocalAngular: boolean) { + return ( + ($route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute) || + (!$route.current && isLocalAngular) + ); +} + +export const configureAppAngularModule = ( + angularModule: IModule, + newPlatform: { + core: CoreStart; + readonly env: { + mode: Readonly; + packageInfo: Readonly; + }; + }, + isLocalAngular: boolean, + getHistory?: () => History +) => { + const core = 'core' in newPlatform ? newPlatform.core : newPlatform; + const packageInfo = newPlatform.env.packageInfo; + + angularModule + .value('osdVersion', packageInfo.version) + .value('buildNum', packageInfo.buildNum) + .value('buildSha', packageInfo.buildSha) + .value('opensearchUrl', getOpenSearchUrl(core)) + .value('uiCapabilities', core.application.capabilities) + .config(setupCompileProvider(newPlatform.env.mode.dev)) + .config(setupLocationProvider()) + .config($setupXsrfRequestInterceptor(packageInfo.version)) + .run(capture$httpLoadingCount(core)) + .run(digestOnHashChange(getHistory)) + .run($setupBreadcrumbsAutoClear(core, isLocalAngular)) + .run($setupBadgeAutoClear(core, isLocalAngular)) + .run($setupHelpExtensionAutoClear(core, isLocalAngular)) + .run($setupUICapabilityRedirect(core)); +}; + +const getOpenSearchUrl = (newPlatform: CoreStart) => { + const a = document.createElement('a'); + a.href = newPlatform.http.basePath.prepend('/opensearch'); + const protocolPort = /https/.test(a.protocol) ? 443 : 80; + const port = a.port || protocolPort; + return { + host: a.hostname, + port, + protocol: a.protocol, + pathname: a.pathname, + }; +}; + +const digestOnHashChange = (getHistory?: () => History) => ($rootScope: IRootScopeService) => { + if (!getHistory) return; + const unlisten = getHistory().listen(() => { + // dispatch synthetic hash change event to update hash history objects and angular routing + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + // this has to happen in the next tick to not change the existing timing of angular digest cycles. + setTimeout(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }, 0); + }); + $rootScope.$on('$destroy', unlisten); +}; + +const setupCompileProvider = (devMode: boolean) => ($compileProvider: ICompileProvider) => { + if (!devMode) { + $compileProvider.debugInfoEnabled(false); + } +}; + +const setupLocationProvider = () => ($locationProvider: ILocationProvider) => { + $locationProvider.html5Mode({ + enabled: false, + requireBase: false, + rewriteLinks: false, + }); + + $locationProvider.hashPrefix(''); +}; + +export const $setupXsrfRequestInterceptor = (version: string) => { + // Configure jQuery prefilter + $.ajaxPrefilter(({ osdXsrfToken = true }: any, originalOptions, jqXHR) => { + if (osdXsrfToken) { + jqXHR.setRequestHeader('osd-xsrf', 'osd-legacy'); + // ToDo: Remove next; `osd-version` incorrectly used for satisfying XSRF protection + jqXHR.setRequestHeader('osd-version', version); + } + }); + + return ($httpProvider: IHttpProvider) => { + // Configure $httpProvider interceptor + $httpProvider.interceptors.push(() => { + return { + request(opts) { + const { osdXsrfToken = true } = opts as any; + if (osdXsrfToken) { + set(opts, ['headers', 'osd-xsrf'], 'osd-legacy'); + // ToDo: Remove next; `osd-version` incorrectly used for satisfying XSRF protection + set(opts, ['headers', 'osd-version'], version); + } + return opts; + }, + }; + }); + }; +}; + +/** + * Injected into angular module by ui/chrome angular integration + * and adds a root-level watcher that will capture the count of + * active $http requests on each digest loop and expose the count to + * the core.loadingCount api + */ +const capture$httpLoadingCount = (newPlatform: CoreStart) => ( + $rootScope: IRootScopeService, + $http: IHttpService +) => { + newPlatform.http.addLoadingCountSource( + new Rx.Observable((observer) => { + const unwatch = $rootScope.$watch(() => { + const reqs = $http.pendingRequests || []; + observer.next(reqs.filter((req) => !isSystemApiRequest(req)).length); + }); + + return unwatch; + }) + ); +}; + +/** + * integrates with angular to automatically redirect to home if required + * capability is not met + */ +const $setupUICapabilityRedirect = (newPlatform: CoreStart) => ( + $rootScope: IRootScopeService, + $injector: any +) => { + const isOpenSearchDashboardsAppRoute = window.location.pathname.endsWith( + '/app/opensearch-dashboards' + ); + // this feature only works within opensearch dashboards app for now after everything is + // switched to the application service, this can be changed to handle all + // apps. + if (!isOpenSearchDashboardsAppRoute) { + return; + } + $rootScope.$on( + '$routeChangeStart', + (event, { $$route: route }: { $$route?: RouteConfiguration } = {}) => { + if (!route || !route.requireUICapability) { + return; + } + + if (!get(newPlatform.application.capabilities, route.requireUICapability)) { + $injector.get('$location').url('/home'); + event.preventDefault(); + } + } + ); +}; + +/** + * internal angular run function that will be called when angular bootstraps and + * lets us integrate with the angular router so that we can automatically clear + * the breadcrumbs if we switch to a OpenSearch Dashboards app that does not use breadcrumbs correctly + */ +const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => ( + $rootScope: IRootScopeService, + $injector: any +) => { + // A flag used to determine if we should automatically + // clear the breadcrumbs between angular route changes. + let breadcrumbSetSinceRouteChange = false; + const $route = $injector.has('$route') ? $injector.get('$route') : {}; + + // reset breadcrumbSetSinceRouteChange any time the breadcrumbs change, even + // if it was done directly through the new platform + newPlatform.chrome.getBreadcrumbs$().subscribe({ + next() { + breadcrumbSetSinceRouteChange = true; + }, + }); + + $rootScope.$on('$routeChangeStart', () => { + breadcrumbSetSinceRouteChange = false; + }); + + $rootScope.$on('$routeChangeSuccess', () => { + if (isDummyRoute($route, isLocalAngular)) { + return; + } + const current = $route.current || {}; + + if (breadcrumbSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { + return; + } + + const k7BreadcrumbsProvider = current.k7Breadcrumbs; + if (!k7BreadcrumbsProvider) { + newPlatform.chrome.setBreadcrumbs([]); + return; + } + + try { + newPlatform.chrome.setBreadcrumbs($injector.invoke(k7BreadcrumbsProvider)); + } catch (error) { + if (isAngularHttpError(error)) { + error = formatAngularHttpError(error); + } + newPlatform.fatalErrors.add(error, 'location'); + } + }); +}; + +/** + * internal angular run function that will be called when angular bootstraps and + * lets us integrate with the angular router so that we can automatically clear + * the badge if we switch to a OpenSearch Dashboards app that does not use the badge correctly + */ +const $setupBadgeAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => ( + $rootScope: IRootScopeService, + $injector: any +) => { + // A flag used to determine if we should automatically + // clear the badge between angular route changes. + let badgeSetSinceRouteChange = false; + const $route = $injector.has('$route') ? $injector.get('$route') : {}; + + $rootScope.$on('$routeChangeStart', () => { + badgeSetSinceRouteChange = false; + }); + + $rootScope.$on('$routeChangeSuccess', () => { + if (isDummyRoute($route, isLocalAngular)) { + return; + } + const current = $route.current || {}; + + if (badgeSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { + return; + } + + const badgeProvider = current.badge; + if (!badgeProvider) { + newPlatform.chrome.setBadge(undefined); + return; + } + + try { + newPlatform.chrome.setBadge($injector.invoke(badgeProvider)); + } catch (error) { + if (isAngularHttpError(error)) { + error = formatAngularHttpError(error); + } + newPlatform.fatalErrors.add(error, 'location'); + } + }); +}; + +/** + * internal angular run function that will be called when angular bootstraps and + * lets us integrate with the angular router so that we can automatically clear + * the helpExtension if we switch to a OpenSearch Dashboards app that does not set its own + * helpExtension + */ +const $setupHelpExtensionAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => ( + $rootScope: IRootScopeService, + $injector: any +) => { + /** + * reset helpExtensionSetSinceRouteChange any time the helpExtension changes, even + * if it was done directly through the new platform + */ + let helpExtensionSetSinceRouteChange = false; + newPlatform.chrome.getHelpExtension$().subscribe({ + next() { + helpExtensionSetSinceRouteChange = true; + }, + }); + + const $route = $injector.has('$route') ? $injector.get('$route') : {}; + + $rootScope.$on('$routeChangeStart', () => { + if (isDummyRoute($route, isLocalAngular)) { + return; + } + helpExtensionSetSinceRouteChange = false; + }); + + $rootScope.$on('$routeChangeSuccess', () => { + if (isDummyRoute($route, isLocalAngular)) { + return; + } + const current = $route.current || {}; + + if (helpExtensionSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { + return; + } + + newPlatform.chrome.setHelpExtension(current.helpExtension); + }); +}; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/index.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/index.ts new file mode 100644 index 0000000000..c492de5100 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/index.ts @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +export { PromiseServiceCreator } from './promises'; +// @ts-ignore +export { watchMultiDecorator } from './watch_multi'; +export * from './angular_config'; +// @ts-ignore +export { createTopNavDirective, createTopNavHelper, loadOsdTopNavDirectives } from './osd_top_nav'; +export { subscribeWithScope } from './subscribe_with_scope'; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/osd_top_nav.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/osd_top_nav.js new file mode 100644 index 0000000000..11835005b6 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/osd_top_nav.js @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import angular from 'angular'; +import 'ngreact'; + +export function createTopNavDirective() { + return { + restrict: 'E', + template: '', + compile: (elem) => { + const child = document.createElement('osd-top-nav-helper'); + + // Copy attributes to the child directive + for (const attr of elem[0].attributes) { + child.setAttribute(attr.name, attr.value); + } + + // Add a special attribute that will change every time that one + // of the config array's disableButton function return value changes. + child.setAttribute('disabled-buttons', 'disabledButtons'); + + // Append helper directive + elem.append(child); + + const linkFn = ($scope, _, $attr) => { + // Watch config changes + $scope.$watch( + () => { + const config = $scope.$eval($attr.config) || []; + return config.map((item) => { + // Copy key into id, as it's a reserved react propery. + // This is done for Angular directive backward compatibility. + // In React only id is recognized. + if (item.key && !item.id) { + item.id = item.key; + } + + // Watch the disableButton functions + if (typeof item.disableButton === 'function') { + return item.disableButton(); + } + return item.disableButton; + }); + }, + (newVal) => { + $scope.disabledButtons = newVal; + }, + true + ); + }; + + return linkFn; + }, + }; +} + +export const createTopNavHelper = ({ TopNavMenu }) => (reactDirective) => { + return reactDirective(TopNavMenu, [ + ['config', { watchDepth: 'value' }], + ['setMenuMountPoint', { watchDepth: 'reference' }], + ['disabledButtons', { watchDepth: 'reference' }], + + ['query', { watchDepth: 'reference' }], + ['savedQuery', { watchDepth: 'reference' }], + ['intl', { watchDepth: 'reference' }], + + ['onQuerySubmit', { watchDepth: 'reference' }], + ['onFiltersUpdated', { watchDepth: 'reference' }], + ['onRefreshChange', { watchDepth: 'reference' }], + ['onClearSavedQuery', { watchDepth: 'reference' }], + ['onSaved', { watchDepth: 'reference' }], + ['onSavedQueryUpdated', { watchDepth: 'reference' }], + ['onSavedQueryIdChange', { watchDepth: 'reference' }], + + ['indexPatterns', { watchDepth: 'collection' }], + ['filters', { watchDepth: 'collection' }], + + // All modifiers default to true. + // Set to false to hide subcomponents. + 'showSearchBar', + 'showQueryBar', + 'showQueryInput', + 'showSaveQuery', + 'showDatePicker', + 'showFilterBar', + + 'appName', + 'screenTitle', + 'dateRangeFrom', + 'dateRangeTo', + 'savedQueryId', + 'isRefreshPaused', + 'refreshInterval', + 'disableAutoFocus', + 'showAutoRefreshOnly', + + // temporary flag to use the stateful components + 'useDefaultBehaviors', + ]); +}; + +let isLoaded = false; + +export function loadOsdTopNavDirectives(navUi) { + if (!isLoaded) { + isLoaded = true; + angular + .module('opensearchDashboards') + .directive('osdTopNav', createTopNavDirective) + .directive('osdTopNavHelper', createTopNavHelper(navUi)); + } +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/promises.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/promises.js new file mode 100644 index 0000000000..690bc5489d --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/promises.js @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export function PromiseServiceCreator($q, $timeout) { + function Promise(fn) { + if (typeof this === 'undefined') + throw new Error('Promise constructor must be called with "new"'); + + const defer = $q.defer(); + try { + fn(defer.resolve, defer.reject); + } catch (e) { + defer.reject(e); + } + return defer.promise; + } + + Promise.all = Promise.props = $q.all; + Promise.resolve = function (val) { + const defer = $q.defer(); + defer.resolve(val); + return defer.promise; + }; + Promise.reject = function (reason) { + const defer = $q.defer(); + defer.reject(reason); + return defer.promise; + }; + Promise.cast = $q.when; + Promise.delay = function (ms) { + return $timeout(_.noop, ms); + }; + Promise.method = function (fn) { + return function () { + const args = Array.prototype.slice.call(arguments); + return Promise.try(fn, args, this); + }; + }; + Promise.nodeify = function (promise, cb) { + promise.then(function (val) { + cb(void 0, val); + }, cb); + }; + Promise.map = function (arr, fn) { + return Promise.all( + arr.map(function (i, el, list) { + return Promise.try(fn, [i, el, list]); + }) + ); + }; + Promise.each = function (arr, fn) { + const queue = arr.slice(0); + let i = 0; + return (function next() { + if (!queue.length) return arr; + return Promise.try(fn, [arr.shift(), i++]).then(next); + })(); + }; + Promise.is = function (obj) { + // $q doesn't create instances of any constructor, promises are just objects with a then function + // https://github.com/angular/angular.js/blob/58f5da86645990ef984353418cd1ed83213b111e/src/ng/q.js#L335 + return obj && typeof obj.then === 'function'; + }; + Promise.halt = _.once(function () { + const promise = new Promise(() => {}); + promise.then = _.constant(promise); + promise.catch = _.constant(promise); + return promise; + }); + Promise.try = function (fn, args, ctx) { + if (typeof fn !== 'function') { + return Promise.reject(new TypeError('fn must be a function')); + } + + let value; + + if (Array.isArray(args)) { + try { + value = fn.apply(ctx, args); + } catch (e) { + return Promise.reject(e); + } + } else { + try { + value = fn.call(ctx, args); + } catch (e) { + return Promise.reject(e); + } + } + + return Promise.resolve(value); + }; + Promise.fromNode = function (takesCbFn) { + return new Promise(function (resolve, reject) { + takesCbFn(function (err, ...results) { + if (err) reject(err); + else if (results.length > 1) resolve(results); + else resolve(results[0]); + }); + }); + }; + Promise.race = function (iterable) { + return new Promise((resolve, reject) => { + for (const i of iterable) { + Promise.resolve(i).then(resolve, reject); + } + }); + }; + + return Promise; +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/subscribe_with_scope.test.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/subscribe_with_scope.test.ts new file mode 100644 index 0000000000..3784988fc8 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/subscribe_with_scope.test.ts @@ -0,0 +1,208 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as Rx from 'rxjs'; +import { subscribeWithScope } from './subscribe_with_scope'; + +// eslint-disable-next-line prefer-const +let $rootScope: Scope; + +class Scope { + public $$phase?: string; + public $root = $rootScope; + public $apply = jest.fn((fn: () => void) => fn()); +} + +$rootScope = new Scope(); + +afterEach(() => { + jest.clearAllMocks(); +}); + +it('subscribes to the passed observable, returns subscription', () => { + const $scope = new Scope(); + + const unsubSpy = jest.fn(); + const subSpy = jest.fn(() => unsubSpy); + const observable = new Rx.Observable(subSpy); + + const subscription = subscribeWithScope($scope as any, observable); + expect(subSpy).toHaveBeenCalledTimes(1); + expect(unsubSpy).not.toHaveBeenCalled(); + + subscription.unsubscribe(); + + expect(subSpy).toHaveBeenCalledTimes(1); + expect(unsubSpy).toHaveBeenCalledTimes(1); +}); + +it('calls observer.next() if already in a digest cycle, wraps in $scope.$apply if not', () => { + const subject = new Rx.Subject(); + const nextSpy = jest.fn(); + const $scope = new Scope(); + + subscribeWithScope($scope as any, subject, { next: nextSpy }); + + subject.next(); + expect($scope.$apply).toHaveBeenCalledTimes(1); + expect(nextSpy).toHaveBeenCalledTimes(1); + + jest.clearAllMocks(); + + $rootScope.$$phase = '$digest'; + subject.next(); + expect($scope.$apply).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(1); +}); + +it('reports fatalError if observer.next() throws', () => { + const fatalError = jest.fn(); + const $scope = new Scope(); + subscribeWithScope( + $scope as any, + Rx.of(undefined), + { + next() { + throw new Error('foo bar'); + }, + }, + fatalError + ); + + expect(fatalError.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + [Error: foo bar], + ], +] +`); +}); + +it('reports fatal error if observer.error is not defined and observable errors', () => { + const fatalError = jest.fn(); + const $scope = new Scope(); + const error = new Error('foo'); + error.stack = `${error.message}\n---stack trace ---`; + subscribeWithScope($scope as any, Rx.throwError(error), undefined, fatalError); + + expect(fatalError.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + [Error: Uncaught error in subscribeWithScope(): foo +---stack trace ---], + ], +] +`); +}); + +it('reports fatal error if observer.error throws', () => { + const fatalError = jest.fn(); + const $scope = new Scope(); + subscribeWithScope( + $scope as any, + Rx.throwError(new Error('foo')), + { + error: () => { + throw new Error('foo'); + }, + }, + fatalError + ); + + expect(fatalError.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + [Error: foo], + ], +] +`); +}); + +it('does not report fatal error if observer.error handles the error', () => { + const fatalError = jest.fn(); + const $scope = new Scope(); + subscribeWithScope( + $scope as any, + Rx.throwError(new Error('foo')), + { + error: () => { + // noop, swallow error + }, + }, + fatalError + ); + + expect(fatalError.mock.calls).toEqual([]); +}); + +it('reports fatal error if observer.complete throws', () => { + const fatalError = jest.fn(); + const $scope = new Scope(); + subscribeWithScope( + $scope as any, + Rx.EMPTY, + { + complete: () => { + throw new Error('foo'); + }, + }, + fatalError + ); + + expect(fatalError.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + [Error: foo], + ], +] +`); +}); + +it('preserves the context of the observer functions', () => { + const $scope = new Scope(); + const observer = { + next() { + expect(this).toBe(observer); + }, + complete() { + expect(this).toBe(observer); + }, + }; + + subscribeWithScope($scope as any, Rx.of([1, 2, 3]), observer); + + const observer2 = { + error() { + expect(this).toBe(observer); + }, + }; + + subscribeWithScope($scope as any, Rx.throwError(new Error('foo')), observer2); +}); diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/subscribe_with_scope.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/subscribe_with_scope.ts new file mode 100644 index 0000000000..f8cb102379 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/subscribe_with_scope.ts @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IScope } from 'angular'; +import * as Rx from 'rxjs'; +import { AngularHttpError } from '../notify/lib'; + +type FatalErrorFn = (error: AngularHttpError | Error | string, location?: string) => void; + +function callInDigest($scope: IScope, fn: () => void, fatalError?: FatalErrorFn) { + try { + // this is terrible, but necessary to synchronously deliver subscription values + // to angular scopes. This is required by some APIs, like the `config` service, + // and beneficial for root level directives where additional digest cycles make + // opensearch dashboards sluggish to load. + // + // If you copy this code elsewhere you better have a good reason :) + if ($scope.$root.$$phase) { + fn(); + } else { + $scope.$apply(() => fn()); + } + } catch (error) { + if (fatalError) { + fatalError(error); + } + } +} + +/** + * Subscribe to an observable at a $scope, ensuring that the digest cycle + * is run for subscriber hooks and routing errors to fatalError if not handled. + */ +export function subscribeWithScope( + $scope: IScope, + observable: Rx.Observable, + observer?: Rx.PartialObserver, + fatalError?: FatalErrorFn +) { + return observable.subscribe({ + next(value) { + if (observer && observer.next) { + callInDigest($scope, () => observer.next!(value), fatalError); + } + }, + error(error) { + callInDigest( + $scope, + () => { + if (observer && observer.error) { + observer.error(error); + } else { + throw new Error( + `Uncaught error in subscribeWithScope(): ${ + error ? error.stack || error.message : error + }` + ); + } + }, + fatalError + ); + }, + complete() { + if (observer && observer.complete) { + callInDigest($scope, () => observer.complete!(), fatalError); + } + }, + }); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/watch_multi.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/watch_multi.js new file mode 100644 index 0000000000..8dfcb0f594 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular/watch_multi.js @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +export function watchMultiDecorator($provide) { + $provide.decorator('$rootScope', function ($delegate) { + /** + * Watch multiple expressions with a single callback. Along + * with making code simpler it also merges all of the watcher + * handlers within a single tick. + * + * # expression format + * expressions can be specified in one of the following ways: + * 1. string that evaluates to a value on scope. Creates a regular $watch + * expression. + * 'someScopeValue.prop' === $scope.$watch('someScopeValue.prop', fn); + * + * 2. #1 prefixed with '[]', which uses $watchCollection rather than $watch. + * '[]expr' === $scope.$watchCollection('expr', fn); + * + * 3. #1 prefixed with '=', which uses $watch with objectEquality turned on + * '=expr' === $scope.$watch('expr', fn, true); + * + * 4. a function that will be called, like a normal function water + * + * 5. an object with any of the properties: + * `get`: the getter called on each iteration + * `deep`: a flag to turn on objectEquality in $watch + * `fn`: the watch registration function ($scope.$watch or $scope.$watchCollection) + * + * @param {array[string|function|obj]} expressions - the list of expressions to $watch + * @param {Function} fn - the callback function + * @return {Function} - an unwatch function, just like the return value of $watch + */ + $delegate.constructor.prototype.$watchMulti = function (expressions, fn) { + if (!Array.isArray(expressions)) { + throw new TypeError('expected an array of expressions to watch'); + } + + if (!_.isFunction(fn)) { + throw new TypeError('expected a function that is triggered on each watch'); + } + const $scope = this; + const vals = new Array(expressions.length); + const prev = new Array(expressions.length); + let fire = false; + let init = 0; + const neededInits = expressions.length; + + // first, register all of the multi-watchers + const unwatchers = expressions.map(function (expr, i) { + expr = normalizeExpression($scope, expr); + if (!expr) return; + + return expr.fn.call( + $scope, + expr.get, + function (newVal, oldVal) { + if (newVal === oldVal) { + init += 1; + } + + vals[i] = newVal; + prev[i] = oldVal; + fire = true; + }, + expr.deep + ); + }); + + // then, the watcher that checks to see if any of + // the other watchers triggered this cycle + let flip = false; + unwatchers.push( + $scope.$watch( + function () { + if (init < neededInits) return init; + + if (fire) { + fire = false; + flip = !flip; + } + return flip; + }, + function () { + if (init < neededInits) return false; + + fn(vals.slice(0), prev.slice(0)); + vals.forEach(function (v, i) { + prev[i] = v; + }); + } + ) + ); + + return function () { + unwatchers.forEach((listener) => listener()); + }; + }; + + function normalizeExpression($scope, expr) { + if (!expr) return; + const norm = { + fn: $scope.$watch, + deep: false, + }; + + if (_.isFunction(expr)) return _.assign(norm, { get: expr }); + if (_.isObject(expr)) return _.assign(norm, expr); + if (!_.isString(expr)) return; + + if (expr.substr(0, 2) === '[]') { + return _.assign(norm, { + fn: $scope.$watchCollection, + get: expr.substr(2), + }); + } + + if (expr.charAt(0) === '=') { + return _.assign(norm, { + deep: true, + get: expr.substr(1), + }); + } + + return _.assign(norm, { get: expr }); + } + + return $delegate; + }); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/bind_html/bind_html.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/bind_html/bind_html.js new file mode 100755 index 0000000000..5e6f2edea6 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/bind_html/bind_html.js @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* eslint-disable */ + +import angular from 'angular'; + +export function initBindHtml() { + angular + .module('ui.bootstrap.bindHtml', []) + + .directive('bindHtmlUnsafe', function() { + return function(scope, element, attr) { + element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); + scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { + element.html(value || ''); + }); + }; + }); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/index.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/index.ts new file mode 100644 index 0000000000..63b0431ebb --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/index.ts @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* eslint-disable */ + +import { once } from 'lodash'; +import angular from 'angular'; + +// @ts-ignore +import { initBindHtml } from './bind_html/bind_html'; +// @ts-ignore +import { initBootstrapTooltip } from './tooltip/tooltip'; + +import tooltipPopup from './tooltip/tooltip_popup.html'; + +import tooltipUnsafePopup from './tooltip/tooltip_html_unsafe_popup.html'; + +export const initAngularBootstrap = once(() => { + /* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.12.1 - 2015-02-20 + * License: MIT + */ + angular.module('ui.bootstrap', [ + 'ui.bootstrap.tpls', + 'ui.bootstrap.bindHtml', + 'ui.bootstrap.tooltip', + ]); + + angular.module('ui.bootstrap.tpls', [ + 'template/tooltip/tooltip-html-unsafe-popup.html', + 'template/tooltip/tooltip-popup.html', + ]); + + initBindHtml(); + initBootstrapTooltip(); + + angular.module('template/tooltip/tooltip-html-unsafe-popup.html', []).run([ + '$templateCache', + function($templateCache: any) { + $templateCache.put('template/tooltip/tooltip-html-unsafe-popup.html', tooltipUnsafePopup); + }, + ]); + + angular.module('template/tooltip/tooltip-popup.html', []).run([ + '$templateCache', + function($templateCache: any) { + $templateCache.put('template/tooltip/tooltip-popup.html', tooltipPopup); + }, + ]); +}); diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/position.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/position.js new file mode 100755 index 0000000000..2f322e2b42 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/position.js @@ -0,0 +1,178 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* eslint-disable */ + +import angular from 'angular'; + +export function initBootstrapPosition() { + angular + .module('ui.bootstrap.position', []) + + /** + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$position', [ + '$document', + '$window', + function($document, $window) { + function getStyle(el, cssprop) { + if (el.currentStyle) { + //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static') === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + const parentOffsetEl = function(element) { + const docDomEl = $document[0]; + let offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function(element) { + const elBCR = this.offset(element); + let offsetParentBCR = { top: 0, left: 0 }; + const offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + const boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left, + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function(element) { + const boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: + boundingClientRect.top + + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: + boundingClientRect.left + + ($window.pageXOffset || $document[0].documentElement.scrollLeft), + }; + }, + + /** + * Provides coordinates for the targetEl in relation to hostEl + */ + positionElements: function(hostEl, targetEl, positionStr, appendToBody) { + const positionStrParts = positionStr.split('-'); + const pos0 = positionStrParts[0]; + const pos1 = positionStrParts[1] || 'center'; + + let hostElPos; + let targetElWidth; + let targetElHeight; + let targetElPos; + + hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); + + targetElWidth = targetEl.prop('offsetWidth'); + targetElHeight = targetEl.prop('offsetHeight'); + + const shiftWidth = { + center: function() { + return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; + }, + left: function() { + return hostElPos.left; + }, + right: function() { + return hostElPos.left + hostElPos.width; + }, + }; + + const shiftHeight = { + center: function() { + return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; + }, + top: function() { + return hostElPos.top; + }, + bottom: function() { + return hostElPos.top + hostElPos.height; + }, + }; + + switch (pos0) { + case 'right': + targetElPos = { + top: shiftHeight[pos1](), + left: shiftWidth[pos0](), + }; + break; + case 'left': + targetElPos = { + top: shiftHeight[pos1](), + left: hostElPos.left - targetElWidth, + }; + break; + case 'bottom': + targetElPos = { + top: shiftHeight[pos0](), + left: shiftWidth[pos1](), + }; + break; + default: + targetElPos = { + top: hostElPos.top - targetElHeight, + left: shiftWidth[pos1](), + }; + break; + } + + return targetElPos; + }, + }; + }, + ]); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip.js new file mode 100755 index 0000000000..086fa6a7d6 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip.js @@ -0,0 +1,434 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* eslint-disable */ + +import angular from 'angular'; + +import { initBootstrapPosition } from './position'; + +export function initBootstrapTooltip() { + initBootstrapPosition(); + /** + * The following features are still outstanding: animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html tooltips, and selector delegation. + */ + angular + .module('ui.bootstrap.tooltip', ['ui.bootstrap.position']) + + /** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ + .provider('$tooltip', function() { + // The default options tooltip and popover. + const defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0, + }; + + // Default hide triggers for each show trigger + const triggerMap = { + mouseenter: 'mouseleave', + click: 'click', + focus: 'blur', + }; + + // The options specified to the provider globally. + const globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + * // place tooltips left instead of top by default + * $tooltipProvider.options( { placement: 'left' } ); + * }); + */ + this.options = function(value) { + angular.extend(globalOptions, value); + }; + + /** + * This allows you to extend the set of trigger mappings available. E.g.: + * + * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + */ + this.setTriggers = function setTriggers(triggers) { + angular.extend(triggerMap, triggers); + }; + + /** + * This is a helper function for translating camel-case to snake-case. + */ + function snake_case(name) { + const regexp = /[A-Z]/g; + const separator = '-'; + return name.replace(regexp, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + /** + * Returns the actual instance of the $tooltip service. + * TODO support multiple triggers + */ + this.$get = [ + '$window', + '$compile', + '$timeout', + '$document', + '$position', + '$interpolate', + function($window, $compile, $timeout, $document, $position, $interpolate) { + return function $tooltip(type, prefix, defaultTriggerShow) { + const options = angular.extend({}, defaultOptions, globalOptions); + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * undefined; otherwise, it uses the `triggerMap` value of the show + * trigger; else it will just use the show trigger. + */ + function getTriggers(trigger) { + const show = trigger || options.trigger || defaultTriggerShow; + const hide = triggerMap[show] || show; + return { + show: show, + hide: hide, + }; + } + + const directiveName = snake_case(type); + + const startSym = $interpolate.startSymbol(); + const endSym = $interpolate.endSymbol(); + const template = + '
' + + '
'; + + return { + restrict: 'EA', + compile: function(tElem, tAttrs) { + const tooltipLinker = $compile(template); + + return function link(scope, element, attrs) { + let tooltip; + let tooltipLinkedScope; + let transitionTimeout; + let popupTimeout; + let appendToBody = angular.isDefined(options.appendToBody) + ? options.appendToBody + : false; + let triggers = getTriggers(undefined); + const hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); + let ttScope = scope.$new(true); + + const positionTooltip = function() { + const ttPosition = $position.positionElements( + element, + tooltip, + ttScope.placement, + appendToBody + ); + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css(ttPosition); + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + ttScope.isOpen = false; + + function toggleTooltipBind() { + if (!ttScope.isOpen) { + showTooltipBind(); + } else { + hideTooltipBind(); + } + } + + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { + return; + } + + prepareTooltip(); + + if (ttScope.popupDelay) { + // Do nothing if the tooltip was already scheduled to pop-up. + // This happens if show is triggered multiple times before any hide is triggered. + if (!popupTimeout) { + popupTimeout = $timeout(show, ttScope.popupDelay, false); + popupTimeout + .then(reposition => reposition()) + .catch(error => { + // if the timeout is canceled then the string `canceled` is thrown. To prevent + // this from triggering an 'unhandled promise rejection' in angular 1.5+ the + // $timeout service explicitly tells $q that the promise it generated is "handled" + // but that does not include down chain promises like the one created by calling + // `popupTimeout.then()`. Because of this we need to ignore the "canceled" string + // and only propagate real errors + if (error !== 'canceled') { + throw error; + } + }); + } + } else { + show()(); + } + } + + function hideTooltipBind() { + scope.$evalAsync(function() { + hide(); + }); + } + + // Show the tooltip popup element. + function show() { + popupTimeout = null; + + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if (transitionTimeout) { + $timeout.cancel(transitionTimeout); + transitionTimeout = null; + } + + // Don't show empty tooltips. + if (!ttScope.content) { + return angular.noop; + } + + createTooltip(); + + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); + ttScope.$digest(); + + positionTooltip(); + + // And show the tooltip. + ttScope.isOpen = true; + ttScope.$digest(); // digest required as $apply is not called + + // Return positioning function as promise callback for correct + // positioning after draw. + return positionTooltip; + } + + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + ttScope.isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel(popupTimeout); + popupTimeout = null; + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if (ttScope.animation) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 500); + } + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltipLinkedScope = ttScope.$new(); + tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { + if (appendToBody) { + $document.find('body').append(tooltip); + } else { + element.after(tooltip); + } + }); + } + + function removeTooltip() { + transitionTimeout = null; + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + if (tooltipLinkedScope) { + tooltipLinkedScope.$destroy(); + tooltipLinkedScope = null; + } + } + + function prepareTooltip() { + prepPlacement(); + prepPopupDelay(); + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe(type, function(val) { + ttScope.content = val; + + if (!val && ttScope.isOpen) { + hide(); + } + }); + + attrs.$observe(prefix + 'Title', function(val) { + ttScope.title = val; + }); + + function prepPlacement() { + const val = attrs[prefix + 'Placement']; + ttScope.placement = angular.isDefined(val) ? val : options.placement; + } + + function prepPopupDelay() { + const val = attrs[prefix + 'PopupDelay']; + const delay = parseInt(val, 10); + ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; + } + + const unregisterTriggers = function() { + element.unbind(triggers.show, showTooltipBind); + element.unbind(triggers.hide, hideTooltipBind); + }; + + function prepTriggers() { + const val = attrs[prefix + 'Trigger']; + unregisterTriggers(); + + triggers = getTriggers(val); + + if (triggers.show === triggers.hide) { + element.bind(triggers.show, toggleTooltipBind); + } else { + element.bind(triggers.show, showTooltipBind); + element.bind(triggers.hide, hideTooltipBind); + } + } + + prepTriggers(); + + const animation = scope.$eval(attrs[prefix + 'Animation']); + ttScope.animation = angular.isDefined(animation) + ? !!animation + : options.animation; + + const appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); + appendToBody = angular.isDefined(appendToBodyVal) + ? appendToBodyVal + : appendToBody; + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if (appendToBody) { + scope.$on( + '$locationChangeSuccess', + function closeTooltipOnLocationChangeSuccess() { + if (ttScope.isOpen) { + hide(); + } + } + ); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel(transitionTimeout); + $timeout.cancel(popupTimeout); + unregisterTriggers(); + removeTooltip(); + ttScope = null; + }); + }; + }, + }; + }; + }, + ]; + }) + + .directive('tooltip', [ + '$tooltip', + function($tooltip) { + return $tooltip('tooltip', 'tooltip', 'mouseenter'); + }, + ]) + + .directive('tooltipPopup', function() { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html', + }; + }) + + .directive('tooltipHtmlUnsafe', [ + '$tooltip', + function($tooltip) { + return $tooltip('tooltipHtmlUnsafe', 'tooltip', 'mouseenter'); + }, + ]) + + .directive('tooltipHtmlUnsafePopup', function() { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html', + }; + }); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip_html_unsafe_popup.html b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip_html_unsafe_popup.html new file mode 100644 index 0000000000..b48bf70498 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip_html_unsafe_popup.html @@ -0,0 +1,4 @@ +
+
+
+
\ No newline at end of file diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip_popup.html b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip_popup.html new file mode 100644 index 0000000000..eed4ca7d93 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/angular_bootstrap/tooltip/tooltip_popup.html @@ -0,0 +1,4 @@ +
+
+
+
\ No newline at end of file diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/index.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/index.ts new file mode 100644 index 0000000000..36fd9a993c --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/index.ts @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './angular'; +export * from './angular_bootstrap'; +export * from './notify'; +export * from './utils'; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/index.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/index.ts new file mode 100644 index 0000000000..934ca937e1 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/index.ts @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './toasts'; +export * from './lib'; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/add_fatal_error.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/add_fatal_error.ts new file mode 100644 index 0000000000..beb6f81e3e --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/add_fatal_error.ts @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FatalErrorsSetup } from '../../../../../core/public'; +import { + AngularHttpError, + formatAngularHttpError, + isAngularHttpError, +} from './format_angular_http_error'; + +export function addFatalError( + fatalErrors: FatalErrorsSetup, + error: AngularHttpError | Error | string, + location?: string +) { + // add support for angular http errors to newPlatformFatalErrors + if (isAngularHttpError(error)) { + error = formatAngularHttpError(error); + } + + fatalErrors.add(error, location); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_angular_http_error.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_angular_http_error.ts new file mode 100644 index 0000000000..68b3701814 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_angular_http_error.ts @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import { IHttpResponse } from 'angular'; + +export type AngularHttpError = IHttpResponse<{ message: string }>; + +export function isAngularHttpError(error: any): error is AngularHttpError { + return ( + error && + typeof error.status === 'number' && + typeof error.statusText === 'string' && + error.data && + typeof error.data.message === 'string' + ); +} + +export function formatAngularHttpError(error: AngularHttpError) { + // is an Angular $http "error object" + if (error.status === -1) { + // status = -1 indicates that the request was failed to reach the server + return i18n.translate( + 'opensearch_dashboards_legacy.notify.fatalError.unavailableServerErrorMessage', + { + defaultMessage: + 'An HTTP request has failed to connect. ' + + 'Please check if the OpenSearch Dashboards server is running and that your browser has a working connection, ' + + 'or contact your system administrator.', + } + ); + } + + return i18n.translate('opensearch_dashboards_legacy.notify.fatalError.errorStatusMessage', { + defaultMessage: 'Error {errStatus} {errStatusText}: {errMessage}', + values: { + errStatus: error.status, + errStatusText: error.statusText, + errMessage: error.data.message, + }, + }); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_msg.test.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_msg.test.js new file mode 100644 index 0000000000..aee1e140d3 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_msg.test.js @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { formatMsg } from './format_msg'; +import expect from '@osd/expect'; + +describe('formatMsg', () => { + test('should prepend the second argument to result', () => { + const actual = formatMsg('error message', 'unit_test'); + + expect(actual).to.equal('unit_test: error message'); + }); + + test('should handle a simple string', () => { + const actual = formatMsg('error message'); + + expect(actual).to.equal('error message'); + }); + + test('should handle a simple Error object', () => { + const err = new Error('error message'); + const actual = formatMsg(err); + + expect(actual).to.equal('error message'); + }); + + test('should handle a simple Angular $http error object', () => { + const err = { + data: { + statusCode: 403, + error: 'Forbidden', + message: + '[security_exception] action [indices:data/read/msearch] is unauthorized for user [user]', + }, + status: 403, + config: {}, + statusText: 'Forbidden', + }; + const actual = formatMsg(err); + + expect(actual).to.equal( + 'Error 403 Forbidden: [security_exception] action [indices:data/read/msearch] is unauthorized for user [user]' + ); + }); + + test('should handle an extended opensearch error', () => { + const err = { + resp: { + error: { + root_cause: [ + { + reason: 'I am the detailed message', + }, + ], + }, + }, + }; + + const actual = formatMsg(err); + + expect(actual).to.equal('I am the detailed message'); + }); +}); diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_msg.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_msg.ts new file mode 100644 index 0000000000..67a4771219 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_msg.ts @@ -0,0 +1,91 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import { i18n } from '@osd/i18n'; +import { formatOpenSearchMsg } from './format_opensearch_msg'; +const has = _.has; + +/** + * Formats the error message from an error object, extended opensearch + * object or simple string; prepends optional second parameter to the message + * @param {Error|String} err + * @param {String} source - Prefix for message indicating source (optional) + * @returns {string} + */ +export function formatMsg(err: Record | string, source: string = '') { + let message = ''; + if (source) { + message += source + ': '; + } + + const opensearchMsg = formatOpenSearchMsg(err); + + if (typeof err === 'string') { + message += err; + } else if (opensearchMsg) { + message += opensearchMsg; + } else if (err instanceof Error) { + message += formatMsg.describeError(err); + } else if (has(err, 'status') && has(err, 'data')) { + // is an Angular $http "error object" + if (err.status === -1) { + // status = -1 indicates that the request was failed to reach the server + message += i18n.translate( + 'opensearch_dashboards_legacy.notify.toaster.unavailableServerErrorMessage', + { + defaultMessage: + 'An HTTP request has failed to connect. ' + + 'Please check if the OpenSearch Dashboards server is running and that your browser has a working connection, ' + + 'or contact your system administrator.', + } + ); + } else { + message += i18n.translate('opensearch_dashboards_legacy.notify.toaster.errorStatusMessage', { + defaultMessage: 'Error {errStatus} {errStatusText}: {errMessage}', + values: { + errStatus: err.status, + errStatusText: err.statusText, + errMessage: err.data.message, + }, + }); + } + } + + return message; +} + +formatMsg.describeError = function (err: Record) { + if (!err) return undefined; + if (err.shortMessage) return err.shortMessage; + if (err.body && err.body.message) return err.body.message; + if (err.message) return err.message; + return '' + err; +}; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_opensearch_msg.test.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_opensearch_msg.test.js new file mode 100644 index 0000000000..bd20b36f35 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_opensearch_msg.test.js @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { formatOpenSearchMsg } from './format_opensearch_msg'; +import expect from '@osd/expect'; + +describe('formatOpenSearchMsg', () => { + test('should return undefined if passed a basic error', () => { + const err = new Error('This is a normal error'); + + const actual = formatOpenSearchMsg(err); + + expect(actual).to.be(undefined); + }); + + test('should return undefined if passed a string', () => { + const err = 'This is a error string'; + + const actual = formatOpenSearchMsg(err); + + expect(actual).to.be(undefined); + }); + + test('should return the root_cause if passed an extended opensearch', () => { + const err = new Error('This is an opensearch error'); + err.resp = { + error: { + root_cause: [ + { + reason: 'I am the detailed message', + }, + ], + }, + }; + + const actual = formatOpenSearchMsg(err); + + expect(actual).to.equal('I am the detailed message'); + }); + + test('should combine the reason messages if more than one is returned.', () => { + const err = new Error('This is an opensearch error'); + err.resp = { + error: { + root_cause: [ + { + reason: 'I am the detailed message 1', + }, + { + reason: 'I am the detailed message 2', + }, + ], + }, + }; + + const actual = formatOpenSearchMsg(err); + + expect(actual).to.equal('I am the detailed message 1\nI am the detailed message 2'); + }); +}); diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_opensearch_msg.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_opensearch_msg.ts new file mode 100644 index 0000000000..b253b949b7 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_opensearch_msg.ts @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; + +const getRootCause = (err: Record | string) => _.get(err, 'resp.error.root_cause'); + +/** + * Utilize the extended error information returned from opensearch + * @param {Error|String} err + * @returns {string} + */ +export const formatOpenSearchMsg = (err: Record | string) => { + const rootCause = getRootCause(err); + + if (!Array.isArray(rootCause)) { + return; + } + + return rootCause.map((cause: Record) => cause.reason).join('\n'); +}; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_stack.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_stack.ts new file mode 100644 index 0000000000..d571ebc69e --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/format_stack.ts @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; + +// browsers format Error.stack differently; always include message +export function formatStack(err: Record) { + if (err.stack && err.stack.indexOf(err.message) === -1) { + return i18n.translate('opensearch_dashboards_legacy.notify.toaster.errorMessage', { + defaultMessage: `Error: {errorMessage} + {errorStack}`, + values: { + errorMessage: err.message, + errorStack: err.stack, + }, + }); + } + return err.stack; +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/index.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/index.ts new file mode 100644 index 0000000000..22a8631dfe --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/lib/index.ts @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { formatOpenSearchMsg } from './format_opensearch_msg'; +export { formatMsg } from './format_msg'; +export { formatStack } from './format_stack'; +export { + isAngularHttpError, + formatAngularHttpError, + AngularHttpError, +} from './format_angular_http_error'; +export { addFatalError } from './add_fatal_error'; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md new file mode 100644 index 0000000000..de6a51f392 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md @@ -0,0 +1,100 @@ +# Toast notifications + +Use this service to surface toasts in the bottom-right corner of the screen. After a brief delay, they'll disappear. They're useful for notifying the user of state changes. See [the EUI docs](https://elastic.github.io/eui/) for more information on toasts and their role within the UI. + +## Importing the module + +```js +import { toastNotifications } from 'ui/notify'; +``` + +## Interface + +### Adding toasts + +For convenience, there are several methods which predefine the appearance of different types of toasts. Use these methods so that the same types of toasts look similar to the user. + +#### Default + +Neutral toast. Tell the user a change in state has occurred, which is not necessarily good or bad. + +```js +toastNotifications.add('Copied to clipboard'); +``` + +#### Success + +Let the user know that an action was successful, such as saving or deleting an object. + +```js +toastNotifications.addSuccess('Your document was saved'); +``` + +#### Warning + +If something OK or good happened, but perhaps wasn't perfect, show a warning toast. + +```js +toastNotifications.addWarning('Your document was saved, but not its edit history'); +``` + +#### Danger + +When the user initiated an action but the action failed, show them a danger toast. + +```js +toastNotifications.addDanger('An error caused your document to be lost'); +``` + +### Removing a toast + +Toasts will automatically be dismissed after a brief delay, but if for some reason you want to dismiss a toast, you can use the returned toast from one of the `add` methods and then pass it to `remove`. + +```js +const toast = toastNotifications.add('Your document was saved'); +toastNotifications.remove(toast); +``` + +### Configuration options + +If you want to configure the toast further you can provide an object instead of a string. The properties of this object correspond to the `propTypes` accepted by the `EuiToast` component. Refer to [the EUI docs](https://elastic.github.io/eui/) for info on these `propTypes`. + +```js +toastNotifications.add({ + title: 'Your document was saved', + text: 'Only you have access to this document', + color: 'success', + iconType: 'check', + 'data-test-subj': 'saveDocumentSuccess', +}); +``` + +Because the underlying components are React, you can use JSX to pass in React elements to the `text` prop. This gives you total flexibility over the content displayed within the toast. + +```js +toastNotifications.add({ + title: 'Your document was saved', + text: ( +
+

+ Only you have access to this document. Edit permissions. +

+ + +
+ ), +}); +``` + +## Use in functional tests + +Functional tests are commonly used to verify that a user action yielded a successful outcome. If you surface a toast to notify the user of this successful outcome, you can place a `data-test-subj` attribute on the toast and use it to check if the toast exists inside of your functional test. This acts as a proxy for verifying the successful outcome. + +```js +toastNotifications.addSuccess({ + title: 'Your document was saved', + 'data-test-subj': 'saveDocumentSuccess', +}); +``` diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/index.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/index.ts new file mode 100644 index 0000000000..71115523dc --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/index.ts @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ToastNotifications } from './toast_notifications'; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/toast_notifications.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/toast_notifications.ts new file mode 100644 index 0000000000..8860c089c6 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/notify/toasts/toast_notifications.ts @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + NotificationsSetup, + Toast, + ToastInput, + ErrorToastOptions, +} from 'opensearch-dashboards/public'; + +export class ToastNotifications { + public list: Toast[] = []; + + private onChangeCallback?: () => void; + + constructor(private readonly toasts: NotificationsSetup['toasts']) { + toasts.get$().subscribe((list) => { + this.list = list; + + if (this.onChangeCallback) { + this.onChangeCallback(); + } + }); + } + + public onChange = (callback: () => void) => { + this.onChangeCallback = callback; + }; + + public add = (toastOrTitle: ToastInput) => this.toasts.add(toastOrTitle); + public remove = (toast: Toast) => this.toasts.remove(toast); + public addSuccess = (toastOrTitle: ToastInput) => this.toasts.addSuccess(toastOrTitle); + public addWarning = (toastOrTitle: ToastInput) => this.toasts.addWarning(toastOrTitle); + public addDanger = (toastOrTitle: ToastInput) => this.toasts.addDanger(toastOrTitle); + public addError = (error: Error, options: ErrorToastOptions) => + this.toasts.addError(error, options); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/index.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/index.ts new file mode 100644 index 0000000000..6313548a1b --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/index.ts @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './system_api'; +// @ts-ignore +export { OsdAccessibleClickProvider } from './osd_accessible_click'; +// @ts-ignore +export { PrivateProvider, IPrivate } from './private'; +// @ts-ignore +export { registerListenEventListener } from './register_listen_event_listener'; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/inject_header_style.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/inject_header_style.ts new file mode 100644 index 0000000000..3ff447aeb9 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/inject_header_style.ts @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IUiSettingsClient } from 'opensearch-dashboards/public'; + +export function buildCSS(maxHeight = 0, truncateGradientHeight = 15) { + return ` +.truncate-by-height { + max-height: ${maxHeight > 0 ? `${maxHeight}px !important` : 'none'}; + display: inline-block; +} +.truncate-by-height:before { + top: ${maxHeight > 0 ? maxHeight - truncateGradientHeight : truncateGradientHeight * -1}px; +} +`; +} + +export function injectHeaderStyle(uiSettings: IUiSettingsClient) { + const style = document.createElement('style'); + style.setAttribute('id', 'style-compile'); + document.getElementsByTagName('head')[0].appendChild(style); + + uiSettings.get$('truncate:maxHeight').subscribe((value: number) => { + // eslint-disable-next-line no-unsanitized/property + style.innerHTML = buildCSS(value); + }); +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/osd_accessible_click.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/osd_accessible_click.js new file mode 100644 index 0000000000..6c49ff8de4 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/osd_accessible_click.js @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { accessibleClickKeys, keys } from '@elastic/eui'; + +export function OsdAccessibleClickProvider() { + return { + restrict: 'A', + controller: ($element) => { + $element.on('keydown', (e) => { + // Prevent a scroll from occurring if the user has hit space. + if (e.key === keys.SPACE) { + e.preventDefault(); + } + }); + }, + link: (scope, element, attrs) => { + // The whole point of this directive is to hack in functionality that native buttons provide + // by default. + const elementType = element.prop('tagName'); + + if (elementType === 'BUTTON') { + throw new Error(`osdAccessibleClick doesn't need to be used on a button.`); + } + + if (elementType === 'A' && attrs.href !== undefined) { + throw new Error( + `osdAccessibleClick doesn't need to be used on a link if it has a href attribute.` + ); + } + + // We're emulating a click action, so we should already have a regular click handler defined. + if (!attrs.ngClick) { + throw new Error('osdAccessibleClick requires ng-click to be defined on its element.'); + } + + // If the developer hasn't already specified attributes required for accessibility, add them. + if (attrs.tabindex === undefined) { + element.attr('tabindex', '0'); + } + + if (attrs.role === undefined) { + element.attr('role', 'button'); + } + + element.on('keyup', (e) => { + // Support keyboard accessibility by emulating mouse click on ENTER or SPACE keypress. + if (accessibleClickKeys[e.key]) { + // Delegate to the click handler on the element (assumed to be ng-click). + element.click(); + } + }); + }, + }; +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/private.d.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/private.d.ts new file mode 100644 index 0000000000..fe264fc193 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/private.d.ts @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type IPrivate = (provider: (...injectable: any[]) => T) => T; diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/private.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/private.js new file mode 100644 index 0000000000..1a3a0a5965 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/private.js @@ -0,0 +1,214 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * # `Private()` + * Private module loader, used to merge angular and require js dependency styles + * by allowing a require.js module to export a single provider function that will + * create a value used within an angular application. This provider can declare + * angular dependencies by listing them as arguments, and can be require additional + * Private modules. + * + * ## Define a private module provider: + * ```js + * export default function PingProvider($http) { + * this.ping = function () { + * return $http.head('/health-check'); + * }; + * }; + * ``` + * + * ## Require a private module: + * ```js + * export default function ServerHealthProvider(Private, Promise) { + * let ping = Private(require('ui/ping')); + * return { + * check: Promise.method(function () { + * let attempts = 0; + * return (function attempt() { + * attempts += 1; + * return ping.ping() + * .catch(function (err) { + * if (attempts < 3) return attempt(); + * }) + * }()) + * .then(function () { + * return true; + * }) + * .catch(function () { + * return false; + * }); + * }) + * } + * }; + * ``` + * + * # `Private.stub(provider, newInstance)` + * `Private.stub()` replaces the instance of a module with another value. This is all we have needed until now. + * + * ```js + * beforeEach(inject(function ($injector, Private) { + * Private.stub( + * // since this module just exports a function, we need to change + * // what Private returns in order to modify it's behavior + * require('ui/agg_response/hierarchical/_build_split'), + * sinon.stub().returns(fakeSplit) + * ); + * })); + * ``` + * + * # `Private.swap(oldProvider, newProvider)` + * This new method does an 1-for-1 swap of module providers, unlike `stub()` which replaces a modules instance. + * Pass the module you want to swap out, and the one it should be replaced with, then profit. + * + * Note: even though this example shows `swap()` being called in a config + * function, it can be called from anywhere. It is particularly useful + * in this scenario though. + * + * ```js + * beforeEach(module('opensearchDashboards', function (PrivateProvider) { + * PrivateProvider.swap( + * function StubbedRedirectProvider($decorate) { + * // $decorate is a function that will instantiate the original module when called + * return sinon.spy($decorate()); + * } + * ); + * })); + * ``` + * + * @param {[type]} prov [description] + */ +import _ from 'lodash'; + +const nextId = _.partial(_.uniqueId, 'privateProvider#'); + +function name(fn) { + return fn.name || fn.toString().split('\n').shift(); +} + +export function PrivateProvider() { + const provider = this; + + // one cache/swaps per Provider + const cache = {}; + const swaps = {}; + + // return the uniq id for this function + function identify(fn) { + if (typeof fn !== 'function') { + throw new TypeError('Expected private module "' + fn + '" to be a function'); + } + + if (fn.$$id) return fn.$$id; + else return (fn.$$id = nextId()); + } + + provider.stub = function (fn, instance) { + cache[identify(fn)] = instance; + return instance; + }; + + provider.swap = function (fn, prov) { + const id = identify(fn); + swaps[id] = prov; + }; + + provider.$get = [ + '$injector', + function PrivateFactory($injector) { + // prevent circular deps by tracking where we came from + const privPath = []; + const pathToString = function () { + return privPath.map(name).join(' -> '); + }; + + // call a private provider and return the instance it creates + function instantiate(prov, locals) { + if (~privPath.indexOf(prov)) { + throw new Error( + 'Circular reference to "' + + name(prov) + + '"' + + ' found while resolving private deps: ' + + pathToString() + ); + } + + privPath.push(prov); + + const context = {}; + let instance = $injector.invoke(prov, context, locals); + if (!_.isObject(instance)) instance = context; + + privPath.pop(); + return instance; + } + + // retrieve an instance from cache or create and store on + function get(id, prov, $delegateId, $delegateProv) { + if (cache[id]) return cache[id]; + + let instance; + + if ($delegateId != null && $delegateProv != null) { + instance = instantiate(prov, { + $decorate: _.partial(get, $delegateId, $delegateProv), + }); + } else { + instance = instantiate(prov); + } + + return (cache[id] = instance); + } + + // main api, get the appropriate instance for a provider + function Private(prov) { + let id = identify(prov); + let $delegateId; + let $delegateProv; + + if (swaps[id]) { + $delegateId = id; + $delegateProv = prov; + + prov = swaps[$delegateId]; + id = identify(prov); + } + + return get(id, prov, $delegateId, $delegateProv); + } + + Private.stub = provider.stub; + Private.swap = provider.swap; + + return Private; + }, + ]; +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/register_listen_event_listener.js b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/register_listen_event_listener.js new file mode 100644 index 0000000000..19652d94cf --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/register_listen_event_listener.js @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function registerListenEventListener($rootScope) { + /** + * Helper that registers an event listener, and removes that listener when + * the $scope is destroyed. + * + * @param {EventEmitter} emitter - the event emitter to listen to + * @param {string} eventName - the event name + * @param {Function} handler - the event handler + * @return {undefined} + */ + $rootScope.constructor.prototype.$listen = function (emitter, eventName, handler) { + emitter.on(eventName, handler); + this.$on('$destroy', function () { + emitter.off(eventName, handler); + }); + }; +} diff --git a/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/system_api.ts b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/system_api.ts new file mode 100644 index 0000000000..2675bbc084 --- /dev/null +++ b/plugins/main/public/kibana-integrations/plugins/opensearch_dashboards_legacy/public/utils/system_api.ts @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IRequestConfig } from 'angular'; + +const SYSTEM_REQUEST_HEADER_NAME = 'osd-system-request'; +const LEGACY_SYSTEM_API_HEADER_NAME = 'osd-system-api'; + +/** + * Adds a custom header designating request as system API + * @param originalHeaders Object representing set of headers + * @return Object representing set of headers, with system API header added in + */ +export function addSystemApiHeader(originalHeaders: Record) { + const systemApiHeaders = { + [SYSTEM_REQUEST_HEADER_NAME]: true, + }; + return { + ...originalHeaders, + ...systemApiHeaders, + }; +} + +/** + * Returns true if request is a system API request; false otherwise + * + * @param request Object Request object created by $http service + * @return true if request is a system API request; false otherwise + */ +export function isSystemApiRequest(request: IRequestConfig) { + const { headers } = request; + return ( + headers && (!!headers[SYSTEM_REQUEST_HEADER_NAME] || !!headers[LEGACY_SYSTEM_API_HEADER_NAME]) + ); +} diff --git a/plugins/main/yarn.lock b/plugins/main/yarn.lock index 915a66d1d9..b04bca1416 100644 --- a/plugins/main/yarn.lock +++ b/plugins/main/yarn.lock @@ -654,11 +654,36 @@ angular-animate@1.8.3: resolved "https://registry.yarnpkg.com/angular-animate/-/angular-animate-1.8.3.tgz#f88db37325de256f9144d1242ce3158134a9d72a" integrity sha512-/LtTKvy5sD6MZbV0v+nHgOIpnFF0mrUp+j5WIxVprVhcrJriYpuCZf4S7Owj1o76De/J0eRzANUozNJ6hVepnQ== +angular-aria@^1.8.0: + version "1.8.3" + resolved "https://registry.yarnpkg.com/angular-aria/-/angular-aria-1.8.3.tgz#b387ebca9569eb557855abb283a09c2d0457e779" + integrity sha512-qTXclmTW/KGw5JNKKQPcCKKq6hCBZ39jYINmLgMsjUHBAoxULaMRRTaRj/L2VTOjKvK5f9enkx+EUqRqzXDSFQ== + angular-material@1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/angular-material/-/angular-material-1.2.5.tgz#6fb3fbf3622d443e4449aaf237d692ad04623a23" integrity sha512-bTTDV0vszpfms1tAMzhLntxBiNMCk/I3Mx/vhbtfhijJILODjpDBfWah0nvWrniFIcxMLcsb1tcPri13hZEaew== +angular-mocks@^1.8.2: + version "1.8.3" + resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.8.3.tgz#c0dd05e5c3fc014e07af6289b23f0e817d7a4724" + integrity sha512-vqsT6zwu80cZ8RY7qRQBZuy6Fq5X7/N5hkV9LzNT0c8b546rw4ErGK6muW1u2JnDKYa7+jJuaGM702bWir4HGw== + +angular-route@^1.8.0: + version "1.8.3" + resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.8.3.tgz#f24a700ebd462454ca83a8b765df55c87a4edde1" + integrity sha512-kpIcRmDR2+o1FxDVVYy8Rvfab86/7LDbOgTRb9T+X9ewPQiBRuDEnZtM3oJYBiQLvAXDYTJXHV48n/bGE9Mv2g== + +angular-sanitize@^1.8.0: + version "1.8.3" + resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.8.3.tgz#51378e990e78c7ecc1fb31cd68655aff690a3bf1" + integrity sha512-2rxdqzlUVafUeWOwvY/FtyWk1pFTyCtzreeiTytG9m4smpuAEKaIJAjYeVwWsoV+nlTOcgpwV4W1OCmR+BQbUg== + +angular@^1.8.2: + version "1.8.3" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.8.3.tgz#851ad75d5163c105a7e329555ef70c90aa706894" + integrity sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -2586,6 +2611,11 @@ next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== +ngreact@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/ngreact/-/ngreact-0.5.2.tgz#d48180b578b186ad70861a3de9ba508b3f22b2ae" + integrity sha512-FCQGtTkDrnI3ywhvK9wUf7C6SYfqKDdRW+cPvy358GFe3AnA4rfvWisDVUQyf5YwNr439ito9xUuuEv80QXhSQ== + node-abi@^3.3.0: version "3.45.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.45.0.tgz#f568f163a3bfca5aacfce1fbeee1fa2cc98441f5" diff --git a/plugins/wazuh-check-updates/opensearch_dashboards.json b/plugins/wazuh-check-updates/opensearch_dashboards.json index 112f05fa5a..bf69812edc 100644 --- a/plugins/wazuh-check-updates/opensearch_dashboards.json +++ b/plugins/wazuh-check-updates/opensearch_dashboards.json @@ -1,6 +1,6 @@ { "id": "wazuhCheckUpdates", - "version": "4.8.1-00", + "version": "4.9.0-00", "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": true, @@ -13,4 +13,4 @@ "optionalPlugins": [ "securityDashboards" ] -} \ No newline at end of file +} diff --git a/plugins/wazuh-check-updates/package.json b/plugins/wazuh-check-updates/package.json index 106451ed81..1f3a34506a 100644 --- a/plugins/wazuh-check-updates/package.json +++ b/plugins/wazuh-check-updates/package.json @@ -1,9 +1,9 @@ { "name": "wazuh-check-updates", - "version": "4.8.1", + "version": "4.9.0", "revision": "00", "pluginPlatform": { - "version": "2.10.0" + "version": "2.11.0" }, "description": "Wazuh Check Updates", "private": true, @@ -29,4 +29,4 @@ "@types/md5": "^2.3.2", "@types/node-cron": "^3.0.8" } -} \ No newline at end of file +} diff --git a/plugins/wazuh-check-updates/yarn.lock b/plugins/wazuh-check-updates/yarn.lock index 77ac1ea0cc..79110b0f40 100644 --- a/plugins/wazuh-check-updates/yarn.lock +++ b/plugins/wazuh-check-updates/yarn.lock @@ -50,10 +50,10 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios@^1.5.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.1.tgz#76550d644bf0a2d469a01f9244db6753208397d7" - integrity sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g== +axios@^1.6.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" + integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" diff --git a/plugins/wazuh-core/opensearch_dashboards.json b/plugins/wazuh-core/opensearch_dashboards.json index a68490fc52..47ff8cfd52 100644 --- a/plugins/wazuh-core/opensearch_dashboards.json +++ b/plugins/wazuh-core/opensearch_dashboards.json @@ -1,6 +1,6 @@ { "id": "wazuhCore", - "version": "4.8.1-00", + "version": "4.9.0-00", "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": true, @@ -11,4 +11,4 @@ "optionalPlugins": [ "securityDashboards" ] -} \ No newline at end of file +} diff --git a/plugins/wazuh-core/package.json b/plugins/wazuh-core/package.json index e0a811f4f9..82522f6cba 100644 --- a/plugins/wazuh-core/package.json +++ b/plugins/wazuh-core/package.json @@ -1,9 +1,9 @@ { "name": "wazuh-core", - "version": "4.8.1", + "version": "4.9.0", "revision": "00", "pluginPlatform": { - "version": "2.10.0" + "version": "2.11.0" }, "description": "Wazuh Core", "private": true, @@ -30,4 +30,4 @@ "@types/": "testing-library/user-event", "@types/md5": "^2.3.2" } -} \ No newline at end of file +} diff --git a/plugins/wazuh-core/yarn.lock b/plugins/wazuh-core/yarn.lock index a28ac899ed..10107cf612 100644 --- a/plugins/wazuh-core/yarn.lock +++ b/plugins/wazuh-core/yarn.lock @@ -45,10 +45,10 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios@^1.5.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.1.tgz#76550d644bf0a2d469a01f9244db6753208397d7" - integrity sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g== +axios@^1.6.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" + integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0"