mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Adding an app for redirects when storing state in session storage (#10822)
* Adding an app for redirects when storing state in session storage * Removing errant console.log * Adding early return after reply().redirect * No longer using the router * Renaming vars to injectedVarsOverrides * Putting uiRoutes back in so the code is only executed for this app * Extracting hash_url to it's own module, and adding tests * Addressing peer-review comments
This commit is contained in:
parent
0be7acc0dd
commit
6d81bafe3d
9 changed files with 279 additions and 6 deletions
13
src/core_plugins/state_session_storage_redirect/index.js
Normal file
13
src/core_plugins/state_session_storage_redirect/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export default function (kibana) {
|
||||
return new kibana.Plugin({
|
||||
uiExports: {
|
||||
app: {
|
||||
require: ['kibana'],
|
||||
title: 'Redirecting',
|
||||
id: 'stateSessionStorageRedirect',
|
||||
main: 'plugins/state_session_storage_redirect',
|
||||
listed: false,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "state_session_storage_redirect",
|
||||
"version": "kibana",
|
||||
"description": "When using the state:storeInSessionStorage setting with the short-urls, we need some way to get the full URL's hashed states into sessionStorage, this app will grab the URL from the kbn-initial-state and and put the URL hashed states into sessionStorage before redirecting the user."
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import 'ui/autoload/styles';
|
||||
import chrome from 'ui/chrome';
|
||||
import { hashUrl } from 'ui/state_management/state_hashing';
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
||||
uiRoutes.enable();
|
||||
uiRoutes
|
||||
.when('/', {
|
||||
resolve: {
|
||||
url: function (AppState, globalState, $window) {
|
||||
const redirectUrl = chrome.getInjected('redirectUrl');
|
||||
|
||||
const hashedUrl = hashUrl([new AppState(), globalState], redirectUrl);
|
||||
const url = chrome.addBasePath(hashedUrl);
|
||||
|
||||
$window.location = url;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -119,7 +119,18 @@ module.exports = async function (kbnServer, server, config) {
|
|||
try {
|
||||
const url = await shortUrlLookup.getUrl(request.params.urlId, request);
|
||||
shortUrlAssertValid(url);
|
||||
reply().redirect(config.get('server.basePath') + url);
|
||||
|
||||
const uiSettings = server.uiSettings();
|
||||
const stateStoreInSessionStorage = await uiSettings.get(request, 'state:storeInSessionStorage');
|
||||
if (!stateStoreInSessionStorage) {
|
||||
reply().redirect(config.get('server.basePath') + url);
|
||||
return;
|
||||
}
|
||||
|
||||
const app = kbnServer.uiExports.apps.byId.stateSessionStorageRedirect;
|
||||
reply.renderApp(app, {
|
||||
redirectUrl: url,
|
||||
});
|
||||
} catch (err) {
|
||||
reply(handleShortUrlError(err));
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ export default async (kbnServer, server, config) => {
|
|||
}
|
||||
});
|
||||
|
||||
async function getKibanaPayload({ app, request, includeUserProvidedConfig }) {
|
||||
async function getKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) {
|
||||
const uiSettings = server.uiSettings();
|
||||
const translations = await uiI18n.getTranslationsForRequest(request);
|
||||
|
||||
|
@ -85,12 +85,12 @@ export default async (kbnServer, server, config) => {
|
|||
vars: await reduceAsync(
|
||||
uiExports.injectedVarsReplacers,
|
||||
async (acc, replacer) => await replacer(acc, request, server),
|
||||
defaults(await app.getInjectedVars() || {}, uiExports.defaultInjectedVars)
|
||||
defaults(injectedVarsOverrides, await app.getInjectedVars() || {}, uiExports.defaultInjectedVars)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function renderApp({ app, reply, includeUserProvidedConfig = true }) {
|
||||
async function renderApp({ app, reply, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) {
|
||||
try {
|
||||
const request = reply.request;
|
||||
const translations = await uiI18n.getTranslationsForRequest(request);
|
||||
|
@ -100,7 +100,8 @@ export default async (kbnServer, server, config) => {
|
|||
kibanaPayload: await getKibanaPayload({
|
||||
app,
|
||||
request,
|
||||
includeUserProvidedConfig
|
||||
includeUserProvidedConfig,
|
||||
injectedVarsOverrides
|
||||
}),
|
||||
bundlePath: `${config.get('server.basePath')}/bundles`,
|
||||
i18n: key => _.get(translations, key, ''),
|
||||
|
@ -110,11 +111,12 @@ export default async (kbnServer, server, config) => {
|
|||
}
|
||||
}
|
||||
|
||||
server.decorate('reply', 'renderApp', function (app) {
|
||||
server.decorate('reply', 'renderApp', function (app, injectedVarsOverrides) {
|
||||
return renderApp({
|
||||
app,
|
||||
reply: this,
|
||||
includeUserProvidedConfig: true,
|
||||
injectedVarsOverrides,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import sinon from 'auto-release-sinon';
|
||||
import { parse as parseUrl } from 'url';
|
||||
|
||||
import StateProvider from 'ui/state_management/state';
|
||||
import { hashUrl } from 'ui/state_management/state_hashing';
|
||||
|
||||
describe('hashUrl', function () {
|
||||
let State;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
|
||||
beforeEach(ngMock.inject((Private, config) => {
|
||||
State = Private(StateProvider);
|
||||
sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(true);
|
||||
}));
|
||||
|
||||
describe('throws error', () => {
|
||||
it('if states parameter is null', () => {
|
||||
expect(() => {
|
||||
hashUrl(null, '');
|
||||
}).to.throwError();
|
||||
});
|
||||
|
||||
it('if states parameter is empty array', () => {
|
||||
expect(() => {
|
||||
hashUrl([], '');
|
||||
}).to.throwError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('does nothing', () => {
|
||||
let states;
|
||||
beforeEach(() => {
|
||||
states = [new State('testParam')];
|
||||
});
|
||||
it('if url is empty', () => {
|
||||
const url = '';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if just a host and port', () => {
|
||||
const url = 'https://localhost:5601';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if just a path', () => {
|
||||
const url = 'https://localhost:5601/app/kibana';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if just a path and query', () => {
|
||||
const url = 'https://localhost:5601/app/kibana?foo=bar';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if empty hash with query', () => {
|
||||
const url = 'https://localhost:5601/app/kibana?foo=bar#';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if query parameter matches and there is no hash', () => {
|
||||
const url = 'https://localhost:5601/app/kibana?testParam=(yes:!t)';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it(`if query parameter matches and it's before the hash`, () => {
|
||||
const url = 'https://localhost:5601/app/kibana?testParam=(yes:!t)';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if empty hash without query', () => {
|
||||
const url = 'https://localhost:5601/app/kibana#';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if empty hash without query', () => {
|
||||
const url = 'https://localhost:5601/app/kibana#';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if hash is just a path', () => {
|
||||
const url = 'https://localhost:5601/app/kibana#/discover';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
|
||||
it('if hash does not have matching query string vals', () => {
|
||||
const url = 'https://localhost:5601/app/kibana#/discover?foo=bar';
|
||||
expect(hashUrl(states, url)).to.be(url);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaces querystring value with hash', () => {
|
||||
const getAppQuery = (url) => {
|
||||
const parsedUrl = parseUrl(url);
|
||||
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true);
|
||||
|
||||
return parsedAppUrl.query;
|
||||
};
|
||||
|
||||
it('if using a single State', () => {
|
||||
const stateParamKey = 'testParam';
|
||||
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey}=(yes:!t)`;
|
||||
const mockHashedItemStore = {
|
||||
getItem: () => null,
|
||||
setItem: sinon.stub().returns(true)
|
||||
};
|
||||
const state = new State(stateParamKey, {}, mockHashedItemStore);
|
||||
|
||||
const actualUrl = hashUrl([state], url);
|
||||
|
||||
expect(mockHashedItemStore.setItem.calledOnce).to.be(true);
|
||||
|
||||
const appQuery = getAppQuery(actualUrl);
|
||||
|
||||
const hashKey = mockHashedItemStore.setItem.firstCall.args[0];
|
||||
expect(appQuery[stateParamKey]).to.eql(hashKey);
|
||||
});
|
||||
|
||||
it('if using multiple States', () => {
|
||||
const stateParamKey1 = 'testParam1';
|
||||
const stateParamKey2 = 'testParam2';
|
||||
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=(yes:!t)&${stateParamKey2}=(yes:!f)`;
|
||||
const mockHashedItemStore = {
|
||||
getItem: () => null,
|
||||
setItem: sinon.stub().returns(true)
|
||||
};
|
||||
const state1 = new State(stateParamKey1, {}, mockHashedItemStore);
|
||||
const state2 = new State(stateParamKey2, {}, mockHashedItemStore);
|
||||
|
||||
const actualUrl = hashUrl([state1, state2], url);
|
||||
|
||||
expect(mockHashedItemStore.setItem.calledTwice).to.be(true);
|
||||
|
||||
const appQuery = getAppQuery(actualUrl);
|
||||
|
||||
const hashKey1 = mockHashedItemStore.setItem.firstCall.args[0];
|
||||
const hashKey2 = mockHashedItemStore.setItem.secondCall.args[0];
|
||||
expect(appQuery[stateParamKey1]).to.eql(hashKey1);
|
||||
expect(appQuery[stateParamKey2]).to.eql(hashKey2);
|
||||
});
|
||||
});
|
||||
});
|
75
src/ui/public/state_management/state_hashing/hash_url.js
Normal file
75
src/ui/public/state_management/state_hashing/hash_url.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import encodeUriQuery from 'encode-uri-query';
|
||||
import rison from 'rison-node';
|
||||
import { parse as parseUrl, format as formatUrl } from 'url';
|
||||
import { stringify as stringifyQuerystring } from 'querystring';
|
||||
|
||||
const conservativeStringifyQuerystring = (query) => {
|
||||
return stringifyQuerystring(query, null, null, {
|
||||
encodeURIComponent: encodeUriQuery
|
||||
});
|
||||
};
|
||||
|
||||
const hashStateInQuery = (state, query) => {
|
||||
const name = state.getQueryParamName();
|
||||
const value = query[name];
|
||||
if (!value) {
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
const decodedValue = rison.decode(value);
|
||||
const hashedValue = state.toQueryParam(decodedValue);
|
||||
return { name, value: hashedValue };
|
||||
};
|
||||
|
||||
const hashStatesInQuery = (states, query) => {
|
||||
const hashedQuery = states.reduce((result, state) => {
|
||||
const { name, value } = hashStateInQuery(state, query);
|
||||
if (value) {
|
||||
result[name] = value;
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
|
||||
return Object.assign({}, query, hashedQuery);
|
||||
};
|
||||
|
||||
export const hashUrl = (states, redirectUrl) => {
|
||||
// we need states to proceed, throwing an error if we don't have any
|
||||
if (states === null || !states.length) {
|
||||
throw new Error('states parameter must be an Array with length greater than 0');
|
||||
}
|
||||
|
||||
const parsedUrl = parseUrl(redirectUrl);
|
||||
// if we don't have a hash, we return the redirectUrl without hashing anything
|
||||
if (!parsedUrl.hash) {
|
||||
return redirectUrl;
|
||||
}
|
||||
|
||||
// The URLs that we use aren't "conventional" and the hash is sometimes appearing before
|
||||
// the querystring, even though conventionally they appear after it. The parsedUrl
|
||||
// is the entire URL, and the parsedAppUrl is everything after the hash.
|
||||
//
|
||||
// EXAMPLE
|
||||
// parsedUrl: /app/kibana#/visualize/edit/somelongguid?g=()&a=()
|
||||
// parsedAppUrl: /visualize/edit/somelongguid?g=()&a=()
|
||||
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true);
|
||||
|
||||
// the parsedAppUrl actually has the query that we care about
|
||||
const query = parsedAppUrl.query;
|
||||
|
||||
const newQuery = hashStatesInQuery(states, query);
|
||||
|
||||
const newHash = formatUrl({
|
||||
search: conservativeStringifyQuerystring(newQuery),
|
||||
pathname: parsedAppUrl.pathname
|
||||
});
|
||||
|
||||
return formatUrl({
|
||||
hash: `#${newHash}`,
|
||||
host: parsedUrl.host,
|
||||
search: parsedUrl.search,
|
||||
pathname: parsedUrl.pathname,
|
||||
protocol: parsedUrl.protocol,
|
||||
});
|
||||
};
|
|
@ -2,6 +2,10 @@ export {
|
|||
default as getUnhashableStatesProvider,
|
||||
} from './get_unhashable_states_provider';
|
||||
|
||||
export {
|
||||
hashUrl,
|
||||
} from './hash_url';
|
||||
|
||||
export {
|
||||
default as unhashQueryString,
|
||||
} from './unhash_query_string';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue