export function getPdfUrl({ id, name: title, width, height }, { pageCount }) { const reportingEntry = chrome.addBasePath('/api/reporting/generate'); const canvasEntry = '/app/canvas#'; // The viewport in Reporting by specifying the dimensions. In order for things to work, // we need a viewport that will include all of the pages in the workpad. The viewport // also needs to include any offset values from the 0,0 position, otherwise the cropped // screenshot that Reporting takes will be off the mark. Reporting will take a screenshot // of the entire viewport and then crop it down to the element that was asked for. // NOTE: while the above is true, the scaling seems to be broken. The export screen draws // pages at the 0,0 point, so the offset isn't currently required to get the correct // viewport size. // build a list of all page urls for exporting, they are captured one at a time const workpadUrls = []; for (let i = 1; i <= pageCount; i++) workpadUrls.push(rison.encode(`${canvasEntry}/export/workpad/pdf/${id}/page/${i}`)); const jobParams = { browserTimezone: 'America/Phoenix', // TODO: get browser timezone, or Kibana setting? layout: { dimensions: { width, height }, id: PDF_LAYOUT_TYPE, }, objectType: 'canvas workpad', relativeUrls: workpadUrls, title, }; return `${reportingEntry}/printablePdf?${QueryString.param( 'jobParams', rison.encode(jobParams) )}`; }
openSingleMetricView = (annotation = {}) => { // Creates the link to the Single Metric Viewer. // Set the total time range from the start to the end of the annotation. const job = this.getJob(annotation.job_id); const dataCounts = job.data_counts; const from = new Date(dataCounts.earliest_record_timestamp).toISOString(); const to = new Date(dataCounts.latest_record_timestamp).toISOString(); const globalSettings = { ml: { jobIds: [job.job_id] }, refreshInterval: { display: 'Off', pause: false, value: 0 }, time: { from, to, mode: 'absolute' } }; const appState = { filters: [], query: { query_string: { analyze_wildcard: true, query: '*' } } }; if (annotation.timestamp !== undefined && annotation.end_timestamp !== undefined) { appState.mlTimeSeriesExplorer = { zoom: { from: new Date(annotation.timestamp).toISOString(), to: new Date(annotation.end_timestamp).toISOString() } }; if (annotation.timestamp < dataCounts.earliest_record_timestamp) { globalSettings.time.from = new Date(annotation.timestamp).toISOString(); } if (annotation.end_timestamp > dataCounts.latest_record_timestamp) { globalSettings.time.to = new Date(annotation.end_timestamp).toISOString(); } } const _g = rison.encode(globalSettings); const _a = rison.encode(appState); const url = `?_g=${_g}&_a=${_a}`; addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); window.open(`${chrome.getBasePath()}/app/ml#/timeseriesexplorer${url}`, '_self'); }
export function getExploreSeriesLink(series) { // Open the Single Metric dashboard over the same overall bounds and // zoomed in to the same time as the current chart. const bounds = timefilter.getActiveBounds(); const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z const to = bounds.max.toISOString(); const zoomFrom = moment(series.plotEarliest).toISOString(); const zoomTo = moment(series.plotLatest).toISOString(); // Pass the detector index and entity fields (i.e. by, over, partition fields) // to identify the particular series to view. // Initially pass them in the mlTimeSeriesExplorer part of the AppState. // TODO - do we want to pass the entities via the filter? const entityCondition = {}; series.entityFields.forEach((entity) => { entityCondition[entity.fieldName] = entity.fieldValue; }); // Use rison to build the URL . const _g = rison.encode({ ml: { jobIds: [series.jobId] }, refreshInterval: { display: 'Off', pause: false, value: 0 }, time: { from: from, to: to, mode: 'absolute' } }); const _a = rison.encode({ mlTimeSeriesExplorer: { zoom: { from: zoomFrom, to: zoomTo }, detectorIndex: series.detectorIndex, entities: entityCondition, }, filters: [], query: { query_string: { analyze_wildcard: true, query: '*' } } }); return `${chrome.getBasePath()}/app/ml#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; }
openSingleMetricView(forecast) { // Creates the link to the Single Metric Viewer. // Set the total time range from the start of the job data to the end of the forecast, const dataCounts = this.props.job.data_counts; const jobEarliest = dataCounts.earliest_record_timestamp; const from = new Date(dataCounts.earliest_record_timestamp).toISOString(); const to = forecast !== undefined ? new Date(forecast.forecast_end_timestamp).toISOString() : new Date(dataCounts.latest_record_timestamp).toISOString(); const _g = rison.encode({ ml: { jobIds: [this.props.job.job_id] }, refreshInterval: { display: 'Off', pause: false, value: 0 }, time: { from, to, mode: 'absolute' } }); const appState = { filters: [], query: { query_string: { analyze_wildcard: true, query: '*' } } }; if (forecast !== undefined) { // Set the zoom to show duration before the forecast equal to the length of the forecast. const forecastDurationMs = forecast.forecast_end_timestamp - forecast.forecast_start_timestamp; const zoomFrom = Math.max(forecast.forecast_start_timestamp - forecastDurationMs, jobEarliest); appState.mlTimeSeriesExplorer = { forecastId: forecast.forecast_id, zoom: { from: new Date(zoomFrom).toISOString(), to: new Date(forecast.forecast_end_timestamp).toISOString() } }; } const _a = rison.encode(appState); const url = `?_g=${_g}&_a=${_a}`; addItemToRecentlyAccessed('timeseriesexplorer', this.props.job.job_id, url); window.open(`${chrome.getBasePath()}/app/ml#/timeseriesexplorer${url}`, '_self'); }
.then((resp) => { let query = null; // Build query using categorization regex (if keyword type) or terms (if text type). // Check for terms or regex in case categoryId represents an anomaly from the absence of the // categorization field in documents (usually indicated by a categoryId of -1). if (categorizationFieldType === ES_FIELD_TYPES.KEYWORD) { if (resp.regex) { query = `${categorizationFieldName}:/${resp.regex}/`; } } else { if (resp.terms) { query = `${categorizationFieldName}:` + resp.terms.split(' ').join(` AND ${categorizationFieldName}:`); } } const recordTime = moment(record.timestamp); const from = recordTime.toISOString(); const to = recordTime.add(record.bucket_span, 's').toISOString(); // Use rison to build the URL . const _g = rison.encode({ refreshInterval: { display: 'Off', pause: false, value: 0 }, time: { from: from, to: to, mode: 'absolute' } }); const appStateProps = { index: indexPatternId, filters: [] }; if (query !== null) { appStateProps.query = { query_string: { analyze_wildcard: true, query: query } }; } const _a = rison.encode(appStateProps); // Need to encode the _a parameter as it will contain characters such as '+' if using the regex. let path = chrome.getBasePath(); path += '/app/kibana#/discover'; path += '?_g=' + _g; path += '&_a=' + encodeURIComponent(_a); window.open(path, '_blank'); }).catch((resp) => {
self._changeLocation = function (type, url, paramObj, replace, appState) { let prev = { path: $location.path(), search: $location.search() }; url = self.eval(url, paramObj); $location[type](url); if (replace) $location.replace(); if (appState) { $location.search('_a', rison.encode(appState)); } let next = { path: $location.path(), search: $location.search() }; if (self._shouldAutoReload(next, prev)) { let appState = getAppState(); if (appState) appState.destroy(); reloading = $rootScope.$on('$locationChangeSuccess', function () { // call the "unlisten" function returned by $on reloading(); reloading = false; $route.reload(); }); } };
State.prototype.translateHashToRison = function (stateHashOrRison) { if (isStateHash(stateHashOrRison)) { return rison.encode(this._parseStateHash(stateHashOrRison)); } return stateHashOrRison; };
State.prototype.toQueryParam = function (state = this.toObject()) { if (!this.isHashingEnabled()) { return rison.encode(state); } // We need to strip out Angular-specific properties. const json = angular.toJson(state); const hash = createStateHash(json, hash => { return this._hashedItemStore.getItem(hash); }); const isItemSet = this._hashedItemStore.setItem(hash, json); if (isItemSet) { return hash; } // If we ran out of space trying to persist the state, notify the user. const message = i18n('common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage', { defaultMessage: 'Kibana is unable to store history items in your session ' + `because it is full and there don't seem to be items any items safe ` + 'to delete.\n\n' + 'This can usually be fixed by moving to a fresh tab, but could ' + 'be caused by a larger issue. If you are seeing this message regularly, ' + 'please file an issue at {gitHubIssuesUrl}.', values: { gitHubIssuesUrl: 'https://github.com/elastic/kibana/issues' } }); fatalError(new Error(message)); };
State.prototype.toQueryParam = function (state = this.toObject()) { if (!this.isHashingEnabled()) { return rison.encode(state); } // We need to strip out Angular-specific properties. const json = angular.toJson(state); const hash = createStateHash(json, hash => { return this._hashedItemStore.getItem(hash); }); const isItemSet = this._hashedItemStore.setItem(hash, json); if (isItemSet) { return hash; } // If we ran out of space trying to persist the state, notify the user. this._notifier.fatal( new Error( 'Kibana is unable to store history items in your session ' + 'because it is full and there don\'t seem to be items any items safe ' + 'to delete.\n' + '\n' + 'This can usually be fixed by moving to a fresh tab, but could ' + 'be caused by a larger issue. If you are seeing this message regularly, ' + 'please file an issue at https://github.com/elastic/kibana/issues.' ) ); };
function initWorkspaceIfRequired() { if ($scope.workspace) { return; } const options = { indexName: $scope.selectedIndex.attributes.title, vertex_fields: $scope.selectedFields, // Here we have the opportunity to look up labels for nodes... nodeLabeller: function () { // console.log(newNodes); }, changeHandler: function () { //Allows DOM to update with graph layout changes. $scope.$apply(); }, graphExploreProxy: callNodeProxy, searchProxy: callSearchNodeProxy, exploreControls: $scope.exploreControls }; $scope.workspace = gws.createWorkspace(options); $scope.detail = null; // filter out default url templates because they will get re-added $scope.urlTemplates = $scope.urlTemplates.filter(template => !template.isDefault); if ($scope.urlTemplates.length === 0) { // url templates specified by users can include the `{{gquery}}` tag and // will have the elasticsearch query for the graph nodes injected there const tag = '{{gquery}}'; const kUrl = new KibanaParsedUrl({ appId: 'kibana', basePath: chrome.getBasePath(), appPath: '/discover' }); kUrl.addQueryParameter('_a', rison.encode({ columns: ['_source'], index: $scope.selectedIndex.id, interval: 'auto', query: tag, sort: ['_score', 'desc'] })); const discoverUrl = kUrl.getRootRelativePath() // replace the URI encoded version of the tag with the unescaped version // so it can be found with String.replace, regexp, etc. .replace(encodeURIComponent(tag), tag); $scope.urlTemplates.push({ url: discoverUrl, description: i18n('xpack.graph.settings.drillDowns.defaultUrlTemplateTitle', { defaultMessage: 'Raw documents', }), encoder: $scope.outlinkEncoders[0], isDefault: true }); } }
function redirectHandler(action) { $location.path('/management/kibana/objects').search({ _a: rison.encode({ tab: serviceObj.title }) }); toastNotifications.addSuccess(`${_.capitalize(action)} '${$scope.obj.attributes.title}' ${$scope.title.toLowerCase()} object`); }
.then(function () { const msg = 'You successfully ' + action + ' the "' + $scope.obj._source.title + '" ' + $scope.title.toLowerCase() + ' object'; $location.path('/management/kibana/objects').search({ _a: rison.encode({ tab: serviceObj.title }) }); notify.info(msg); });
$scope.vizLocation = function (field) { if (!$scope.state) {return '';} let agg = {}; const isGeoPoint = field.type === 'geo_point'; const type = isGeoPoint ? 'tile_map' : 'histogram'; // If we're visualizing a date field, and our index is time based (and thus has a time filter), // then run a date histogram if (field.type === 'date' && $scope.indexPattern.timeFieldName === field.name) { agg = { type: 'date_histogram', schema: 'segment', params: { field: field.name, interval: 'auto' } }; } else if (isGeoPoint) { agg = { type: 'geohash_grid', schema: 'segment', params: { field: field.name, precision: 3 } }; } else { agg = { type: 'terms', schema: 'segment', params: { field: field.name, size: config.get('discover:aggs:terms:size', 20), orderBy: '2' } }; } return '#/visualize/create?' + $.param(_.assign($location.search(), { indexPattern: $scope.state.index, type: type, _a: rison.encode({ filters: $scope.state.filters || [], query: $scope.state.query || undefined, vis: { type: type, aggs: [ agg, {schema: 'metric', type: 'count', 'id': '2'} ] } }) })); };
State.prototype._readFromURL = function () { let search = $location.search(); try { return search[this._urlParam] ? rison.decode(search[this._urlParam]) : null; } catch (e) { notify.error('Unable to parse URL'); search[this._urlParam] = rison.encode(this._defaults); $location.search(search).replace(); return null; } };
export function KibanaLinkComponent({ location, pathname, hash, query, ...props }) { const currentQuery = toQuery(location.search); const nextQuery = { _g: query._g ? rison.encode(query._g) : currentQuery._g, _a: query._a ? rison.encode(query._a) : '' }; const search = stringifyWithoutEncoding(nextQuery); const href = url.format({ pathname: chrome.addBasePath(pathname), hash: `${hash}?${search}` }); return <EuiLink {...props} href={href} />; }
it('should replace rison in the URL with a hash', () => { const { state, hashedItemStore } = setup({ storeInHash: true }); const obj = { foo: { bar: 'baz' } }; const rison = encodeRison(obj); $location.search({ _s: rison }); state.fetch(); const urlVal = $location.search()._s; expect(urlVal).to.not.be(rison); expect(isStateHash(urlVal)).to.be(true); expect(hashedItemStore.getItem(urlVal)).to.eql(JSON.stringify(obj)); });
$scope.getContextAppHref = () => { const path = kbnUrl.eval('#/context/{{ indexPattern }}/{{ anchorType }}/{{ anchorId }}', { anchorId: $scope.row._id, anchorType: $scope.row._type, indexPattern: $scope.indexPattern.id, }); const hash = $httpParamSerializer({ _a: rison.encode({ columns: $scope.columns, }), }); return `${path}?${hash}`; };
async navigateTo(indexPattern, anchorType, anchorId, overrideInitialState = {}) { const initialState = rison.encode({ ...DEFAULT_INITIAL_STATE, ...overrideInitialState, }); const appUrl = getUrl.noAuth(config.get('servers.kibana'), { ...config.get('apps.context'), hash: `${config.get('apps.context.hash')}/${indexPattern}/${anchorType}/${anchorId}?_a=${initialState}`, }); await remote.get(appUrl); await remote.refresh(); await this.waitUntilContextLoadingHasFinished(); }
async navigateTo(indexPattern, anchorType, anchorId, overrideInitialState = {}) { const initialState = rison.encode({ ...DEFAULT_INITIAL_STATE, ...overrideInitialState, }); const appUrl = getUrl.noAuth(config.get('servers.kibana'), { ...config.get('apps.context'), hash: `${config.get('apps.context.hash')}/${indexPattern}/${anchorType}/${anchorId}?_a=${initialState}`, }); await remote.get(appUrl); await this.waitUntilContextLoadingHasFinished(); // For lack of a better way, using a sleep to ensure page is loaded before proceeding await PageObjects.common.sleep(1000); }
.then((response) => { // Use the filters from the saved dashboard if there are any. let filters = []; // Use the query from the dashboard only if no job entities are selected. let query = undefined; const searchSourceJSON = response.get('kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON !== undefined) { const searchSourceData = JSON.parse(searchSourceJSON); if (searchSourceData.filter !== undefined) { filters = searchSourceData.filter; } query = searchSourceData.query; } // Add time settings to the global state URL parameter with $earliest$ and // $latest$ tokens which get substituted for times around the time of the // anomaly on which the URL will be run against. const _g = rison.encode({ time: { from: '$earliest$', to: '$latest$', mode: 'absolute' } }); const appState = { filters }; // To put entities in filters section would involve creating parameters of the form // filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:b30fd340-efb4-11e7-a600-0f58b1422b87, // key:airline,negate:!f,params:(query:AAL,type:phrase),type:phrase,value:AAL),query:(match:(airline:(query:AAL,type:phrase))))) // which includes the ID of the index holding the field used in the filter. // So for simplicity, put entities in the query, replacing any query which is there already. // e.g. query:(language:lucene,query:'region:us-east-1%20AND%20instance:i-20d061fa') if (queryFieldNames !== undefined && queryFieldNames.length > 0) { let queryString = ''; queryFieldNames.forEach((fieldName, index) => { if (index > 0) { queryString += ' AND '; } queryString += `${escapeForElasticsearchQuery(fieldName)}:"$${fieldName}$"`; }); query = { language: 'lucene', query: queryString }; } if (query !== undefined) { appState.query = query; } const _a = rison.encode(appState); const urlValue = `kibana#/dashboard/${dashboardId}?_g=${_g}&_a=${_a}`; const urlToAdd = { url_name: settings.label, url_value: urlValue, time_range: TIME_RANGE_TYPE.AUTO }; if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) { urlToAdd.time_range = settings.timeRange.interval; } resolve(urlToAdd); })
function buildDiscoverUrlFromSettings(settings) { const { discoverIndexPatternId, queryFieldNames } = settings.kibanaSettings; // Add time settings to the global state URL parameter with $earliest$ and // $latest$ tokens which get substituted for times around the time of the // anomaly on which the URL will be run against. const _g = rison.encode({ time: { from: '$earliest$', to: '$latest$', mode: 'absolute' } }); // Add the index pattern and query to the appState part of the URL. const appState = { index: discoverIndexPatternId }; // If partitioning field entities have been configured add tokens // to the URL to use in the Discover page search. // Ideally we would put entities in the filters section, but currently this involves creating parameters of the form // filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:b30fd340-efb4-11e7-a600-0f58b1422b87, // key:airline,negate:!f,params:(query:AAL,type:phrase),type:phrase,value:AAL),query:(match:(airline:(query:AAL,type:phrase))))) // which includes the ID of the index holding the field used in the filter. // So for simplicity, put entities in the query, replacing any query which is there already. // e.g. query:(language:lucene,query:'region:us-east-1%20AND%20instance:i-20d061fa') if (queryFieldNames !== undefined && queryFieldNames.length > 0) { let queryString = ''; queryFieldNames.forEach((fieldName, i) => { if (i > 0) { queryString += ' AND '; } queryString += `${escapeForElasticsearchQuery(fieldName)}:"$${fieldName}$"`; }); appState.query = { language: 'lucene', query: queryString }; } const _a = rison.encode(appState); const urlValue = `kibana#/discover?_g=${_g}&_a=${_a}`; const urlToAdd = { url_name: settings.label, url_value: urlValue, time_range: TIME_RANGE_TYPE.AUTO, }; if (settings.timeRange.type === TIME_RANGE_TYPE.INTERVAL) { urlToAdd.time_range = settings.timeRange.interval; } return urlToAdd; }
it('should be true if state exists', function () { const query = {}; query[stateIndices.global] = rison.encode({ hello: 'world' }); const state = parseKibanaState(query, 'global'); expect(state.exists).to.equal(true); });
const encodedStates = states.map(state => encodeURIComponent(rison.encode(state)));
toString() { return rison.encode(this.state); }
State.prototype.translateHashToRison = function (hash) { return rison.encode(this._parseQueryParamValue(hash)); };
BaseObject.prototype.toRISON = function () { // Use Angular to remove the private vars, and JSON.stringify to serialize return rison.encode(JSON.parse(angular.toJson(this))); };
export function encodeKibanaSearchParams(query) { return stringifyWithoutEncoding({ _g: rison.encode(query._g), _a: rison.encode(query._a) }); }
viewSeries = () => { const record = this.props.anomaly.source; const bounds = this.props.timefilter.getActiveBounds(); const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z const to = bounds.max.toISOString(); // Zoom to show 50 buckets either side of the record. const recordTime = moment(record.timestamp); const zoomFrom = recordTime.subtract(50 * record.bucket_span, 's').toISOString(); const zoomTo = recordTime.add(100 * record.bucket_span, 's').toISOString(); // Extract the by, over and partition fields for the record. const entityCondition = {}; if (_.has(record, 'partition_field_value')) { entityCondition[record.partition_field_name] = record.partition_field_value; } if (_.has(record, 'over_field_value')) { entityCondition[record.over_field_name] = record.over_field_value; } if (_.has(record, 'by_field_value')) { // Note that analyses with by and over fields, will have a top-level by_field_name, // but the by_field_value(s) will be in the nested causes array. // TODO - drilldown from cause in expanded row only? entityCondition[record.by_field_name] = record.by_field_value; } // Use rison to build the URL . const _g = rison.encode({ ml: { jobIds: [record.job_id] }, refreshInterval: { display: 'Off', pause: false, value: 0 }, time: { from: from, to: to, mode: 'absolute' } }); const _a = rison.encode({ mlTimeSeriesExplorer: { zoom: { from: zoomFrom, to: zoomTo }, detectorIndex: record.detector_index, entities: entityCondition, }, query: { query_string: { analyze_wildcard: true, query: '*' } } }); // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'. let path = `${chrome.getBasePath()}/app/ml#/timeseriesexplorer`; path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`; window.open(path, '_blank'); }
self.manageObjects = function (type) { $location.url('/management/kibana/objects?_a=' + rison.encode({ tab: type })); };
it('returns false for RISON', () => { // We're storing RISON in the URL, so let's test against this specifically. const rison = encodeRison({ a: 'a' }); expect(isStateHash(rison)).to.be(false); });