Major refactoring of frontend code to use JWT

This commit is contained in:
Ivan Meredith 2016-08-17 14:37:20 +12:00
parent 6792a9e93f
commit aa4b8bd1df
7 changed files with 88 additions and 200 deletions

View File

@ -37,9 +37,9 @@
"dependencies": {
"@jenkins-cd/design-language": "0.0.68",
"@jenkins-cd/js-extensions": "0.0.21-beta4",
"@jenkins-cd/blueocean-core-js": "file:../blueocean-core-js",
"@jenkins-cd/js-modules": "0.0.5",
"@jenkins-cd/sse-gateway": "0.0.7",
"@jenkins-cd/blueocean-core-js": "0.0.1",
"immutable": "3.8.1",
"isomorphic-fetch": "2.2.1",
"jsonwebtoken": "7.1.8",
@ -48,8 +48,8 @@
"moment-duration-format": "1.3.0",
"moment-timezone": "^0.5.5",
"react": "15.1.0",
"react-dom": "15.1.0",
"react-addons-update": "15.1.0",
"react-dom": "15.1.0",
"react-material-icons-blue": "1.0.4",
"react-redux": "4.4.5",
"react-router": "2.3.0",

View File

@ -5,11 +5,7 @@ import { State } from '../components/records';
import { getNodesInformation } from '../util/logDisplayHelper';
import { calculateStepsBaseUrl, calculateLogUrl, calculateNodeBaseUrl } from '../util/UrlUtils';
import { FetchUtils, JWT } from '@jenkins-cd/blueocean-core-js';
const { checkStatus, fetchOptions, parseJSON } = FetchUtils;
import { FetchUtils } from '@jenkins-cd/blueocean-core-js';
/**
* This function maps a queue item into a run instancce.
@ -19,6 +15,7 @@ const { checkStatus, fetchOptions, parseJSON } = FetchUtils;
* as the same thing. If the raw data is needed if can be fetched
* from _item.
*/
function _mapQueueToPsuedoRun(run) {
if (run._class === 'io.jenkins.blueocean.service.embedded.rest.QueueItemImpl') {
return {
@ -183,18 +180,10 @@ exports.fetchLogsInjectStart = function fetchLogsInjectStart(url, start, onSucce
} else {
refetchUrl = `${url}?start=${start}`;
}
return JWT.getToken()
.then(token => fetch(refetchUrl, fetchOptions(token)))
.then(checkStatus)
return FetchUtils.fetchJson(refetchUrl)
.then(parseMoreDataHeader)
.then(onSuccess)
.catch((error) => {
if (onError) {
onError(error);
} else {
console.error(error); // eslint-disable-line no-console
}
});
.catch(FetchUtils.onError(onError));
};
/**
* Clone a JSON object/array instance.
@ -328,6 +317,7 @@ export const actions = {
};
},
updateRunState(event, config, updateByQueueId) {
return (dispatch, getState) => {
let storeData;
@ -433,48 +423,50 @@ export const actions = {
// The event tells us that the run state has changed, but does not give all
// run related data (times, commit Ids etc). So, lets go get that data from
// REST API and present a consistent picture of the run state to the user.
exports.fetchJson(runUrl, updateRunData, (error) => {
let runData;
FetchUtils.fetchJson(runUrl)
.then(updateRunData)
.catch((error) => {
let runData;
// Getting the actual state of the run failed. Lets log
// the failure and update the state manually as best we can.
// Getting the actual state of the run failed. Lets log
// the failure and update the state manually as best we can.
// eslint-disable-next-line no-console
console.warn(`Error getting run data from REST endpoint: ${runUrl}`);
// eslint-disable-next-line no-console
console.warn(error);
// eslint-disable-next-line no-console
console.warn(`Error getting run data from REST endpoint: ${runUrl}`);
// eslint-disable-next-line no-console
console.warn(error);
// We're after coming out of an async operation (the fetch).
// In that case, we better refresh the copy of the storeData
// that we have in case things changed while we were doing the
// fetch.
storeData = getFromStore();
// We're after coming out of an async operation (the fetch).
// In that case, we better refresh the copy of the storeData
// that we have in case things changed while we were doing the
// fetch.
storeData = getFromStore();
if (storeData.runIndex !== undefined) {
runData = storeData.eventJobRuns[storeData.runIndex];
} else {
runData = {};
runData.job_run_queueId = event.job_run_queueId;
if (event.job_ismultibranch) {
runData.pipeline = event.blueocean_job_branch_name;
if (storeData.runIndex !== undefined) {
runData = storeData.eventJobRuns[storeData.runIndex];
} else {
runData.pipeline = event.blueocean_job_pipeline_name;
runData = {};
runData.job_run_queueId = event.job_run_queueId;
if (event.job_ismultibranch) {
runData.pipeline = event.blueocean_job_branch_name;
} else {
runData.pipeline = event.blueocean_job_pipeline_name;
}
}
}
if (event.jenkins_event === 'job_run_ended') {
runData.state = 'FINISHED';
} else {
runData.state = 'RUNNING';
}
runData.id = event.jenkins_object_id;
runData.result = event.job_run_status;
if (event.jenkins_event === 'job_run_ended') {
runData.state = 'FINISHED';
} else {
runData.state = 'RUNNING';
}
runData.id = event.jenkins_object_id;
runData.result = event.job_run_status;
// Update the run data. We do not need updateRunData to refresh the
// storeData again because we already just did it at the start of
// this function call.
updateRunData(runData, false);
});
// Update the run data. We do not need updateRunData to refresh the
// storeData again because we already just did it at the start of
// this function call.
updateRunData(runData, false);
});
}
};
},
@ -512,9 +504,7 @@ export const actions = {
});
};
exports.fetchJson(url, processBranchData, (error) => {
console.log(error); // eslint-disable-line no-console
});
FetchUtils.fetchJson(url).then(processBranchData).catch(FetchUtils.consoleError);
}
};
},
@ -534,7 +524,7 @@ export const actions = {
// Fetch/refetch the latest set of branches for the pipeline.
const url = `${config.getAppURLBase()}/rest/organizations/${event.jenkins_org}` +
`/pipelines/${pipelineName}/branches`;
exports.fetchJson(url, (latestPipelineBranches) => {
FetchUtils.fetchJson(url).then((latestPipelineBranches) => {
if (event.blueocean_is_for_current_job) {
dispatch({
id: pipelineName,
@ -547,11 +537,12 @@ export const actions = {
payload: latestPipelineBranches,
type: ACTION_TYPES.SET_BRANCHES_DATA,
});
});
}).catch(FetchUtils.consoleError);
}
};
},
fetchRunsIfNeeded(config) {
return (dispatch) => {
const baseUrl = `${config.getAppURLBase()}/rest/organizations/jenkins` +
@ -597,10 +588,7 @@ export const actions = {
const id = general.id;
if (!data || !data[id]) {
return JWT.getToken()
.then(token => fetch(general.url, fetchOptions(token)))
.then(checkStatus)
.then(parseJSON)
return FetchUtils.fetchJson(general.url)
.then(json => {
// TODO: Why call dispatch twice here?
dispatch({
@ -632,11 +620,9 @@ export const actions = {
};
},
generateData(url, actionType, optional) {
return (dispatch) => JWT.getToken()
.then(token => fetch(url, fetchOptions(token)))
.then(checkStatus)
.then(parseJSON)
return (dispatch) => FetchUtils.fetchJson(url)
.then(json => dispatch({
...optional,
type: actionType,
@ -662,6 +648,7 @@ export const actions = {
We later store them with the key: nodesBaseUrl
so we only fetch them once.
*/
fetchNodes(config) {
return (dispatch, getState) => {
const data = getState().adminStore.nodes;
@ -692,9 +679,8 @@ export const actions = {
}
if (!data || !data[nodesBaseUrl] || config.refetch) {
return exports.fetchJson(
nodesBaseUrl,
(json) => {
return FetchUtils.fetchJson(nodesBaseUrl)
.then((json) => {
const information = getNodesInformation(json);
information.nodesBaseUrl = nodesBaseUrl;
dispatch({
@ -703,9 +689,7 @@ export const actions = {
});
return getNodeAndSteps(information);
},
(error) => console.error('error', error) // eslint-disable-line no-console
);
}).catch(FetchUtils.consoleError);
}
return getNodeAndSteps(data[nodesBaseUrl]);
};
@ -742,18 +726,15 @@ export const actions = {
const data = getState().adminStore.steps;
const stepBaseUrl = calculateStepsBaseUrl(config);
if (!data || !data[stepBaseUrl] || config.refetch) {
return exports.fetchJson(
stepBaseUrl,
(json) => {
const information = getNodesInformation(json);
information.nodesBaseUrl = stepBaseUrl;
return dispatch({
type: ACTION_TYPES.SET_STEPS,
payload: information,
});
},
(error) => console.error('error', error) // eslint-disable-line no-console
);
return FetchUtils.fetchJson(stepBaseUrl)
.then((json) => {
const information = getNodesInformation(json);
information.nodesBaseUrl = stepBaseUrl;
return dispatch({
type: ACTION_TYPES.SET_STEPS,
payload: information,
});
}).catch(FetchUtils.consoleError);
}
return null;
};

View File

@ -1,27 +0,0 @@
//
// General "system" config information.
//
// TODO: This should be in a general sharable component.
// Passing it around in the react context is silly.
//
exports.blueoceanAppURL = '/';
exports.jenkinsRootURL = '';
exports.loadConfig = function () {
try {
const headElement = document.getElementsByTagName('head')[0];
// Look up where the Blue Ocean app is hosted
exports.blueoceanAppURL = headElement.getAttribute('data-appurl');
if (typeof exports.blueoceanAppURL !== 'string') {
exports.blueoceanAppURL = '/';
}
exports.jenkinsRootURL = headElement.getAttribute('data-rooturl');
} catch (error) {
// eslint-disable-next-line no-console
console.warn('error reading attributes from document; urls will be empty');
}
};

View File

@ -1,93 +1,21 @@
/**
* Created by cmeyers on 7/6/16.
*/
import fetch from 'isomorphic-fetch';
import { ACTION_TYPES } from './FavoritesStore';
import urlConfig from '../config';
import moment from 'moment-timezone';
import jwt from 'jsonwebtoken';
urlConfig.loadConfig();
const defaultFetchOptions = {
credentials: 'same-origin',
};
function authOptions(opts, token) {
if(!opts.headers) {
opts.headers = {};
}
opts.headers['Authorization']= 'Bearer ' + token
return opts
}
function checkStatus(response) {
if (response.status >= 300 || response.status < 200) {
const error = new Error(response.statusText);
error.response = response;
throw error;
}
return response;
}
function parseJSON(response) {
return response.json()
// FIXME: workaround for status=200 w/ empty response body that causes error in Chrome
// server should probably return HTTP 204 instead
.catch((error) => {
if (error.message === 'Unexpected end of JSON input') {
return {};
}
throw error;
});
}
import { UrlUtils, FetchUtils } from '@jenkins-cd/blueocean-core-js';
const fetchFlags = {
[ACTION_TYPES.SET_USER]: false,
[ACTION_TYPES.SET_FAVORITES]: false,
};
let jwtToken = null;
function storeToken(token) {
jwtToken = token;
return token;
}
function getTokenFromStorage() {
return jwtToken;
}
function getToken() {
const storedToken = getTokenFromStorage();
if (storedToken) {
const tokenPayload = jwt.decode(storedToken);
const expiry = moment.unix(tokenPayload.exp);
if (expiry.diff(moment.tz('UTC'), 'seconds') < 300) {
return Promise.fulfilled(storedToken);
}
}
return fetch('/jenkins/jwt-auth/token', { credentials: 'same-origin' })
.then(checkStatus)
.then(response => {
if (response.headers.get("X-BLUEOCEAN-JWT")) {
const token = response.headers.get("X-BLUEOCEAN-JWT");
storeToken(token);
return token;
}
throw new Error('Could not fetch jwt_token');
});
}
export const actions = {
fetchUser() {
return (dispatch) => {
const baseUrl = urlConfig.blueoceanAppURL;
const baseUrl = UrlUtils.getBlueAppUrl();
const url = `${baseUrl}/rest/organizations/jenkins/user/`;
const fetchOptions = { ...defaultFetchOptions };
if (fetchFlags[ACTION_TYPES.SET_USER]) {
return null;
}
@ -95,7 +23,7 @@ export const actions = {
fetchFlags[ACTION_TYPES.SET_USER] = true;
return dispatch(actions.generateData(
{ url, fetchOptions },
{ url },
ACTION_TYPES.SET_USER
));
};
@ -103,11 +31,10 @@ export const actions = {
fetchFavorites(user) {
return (dispatch) => {
const baseUrl = urlConfig.blueoceanAppURL;
const baseUrl = UrlUtils.getBlueAppUrl();
const username = user.id;
const url = `${baseUrl}/rest/users/${username}/favorites/`;
const fetchOptions = { ...defaultFetchOptions };
if (fetchFlags[ACTION_TYPES.SET_FAVORITES]) {
return null;
}
@ -115,7 +42,7 @@ export const actions = {
fetchFlags[ACTION_TYPES.SET_FAVORITES] = true;
return dispatch(actions.generateData(
{ url, fetchOptions },
{ url },
ACTION_TYPES.SET_FAVORITES
));
};
@ -123,14 +50,13 @@ export const actions = {
toggleFavorite(addFavorite, branch, favoriteToRemove) {
return (dispatch) => {
const baseUrl = urlConfig.jenkinsRootURL;
const baseUrl = UrlUtils.getBlueAppUrl();
const url = addFavorite ?
`${baseUrl}${branch._links.self.href}/favorite` :
`${baseUrl}${favoriteToRemove._links.self.href}`;
const fetchOptions = {
...defaultFetchOptions,
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@ -159,10 +85,7 @@ export const actions = {
generateData(request, actionType, optional) {
const { url, fetchOptions } = request;
return (dispatch) => getToken()
.then(token => fetch(url, authOptions(fetchOptions, token)))
.then(checkStatus)
.then(parseJSON)
return (dispatch) => FetchUtils.fetchJson(url, { fetchOptions })
.then((json) => {
fetchFlags[actionType] = false;
return dispatch({

View File

@ -3,6 +3,7 @@
//
var gi = require('giti');
var fs = require('fs');
var builder = require('@jenkins-cd/js-builder');
// create a dummy revisionInfo so developmentFooter will not fail
@ -24,7 +25,11 @@ gi(function (err, result) {
// Explicitly setting the src paths in order to allow the rebundle task to
// watch for changes in the JDL (js, css, icons etc).
// See https://github.com/jenkinsci/js-builder#setting-src-and-test-spec-paths
builder.src(['src/main/js', 'src/main/less', 'node_modules/@jenkins-cd/design-language/dist']);
builder.src([
'src/main/js',
'src/main/less',
'node_modules/@jenkins-cd/design-language/dist',
'node_modules/@jenkins-cd/blueocean-core-js/dist']);
//
// Create the main "App" bundle.

View File

@ -17,6 +17,11 @@ exports.initialize = function (oncomplete) {
const jdl = require('@jenkins-cd/design-language');
jenkinsMods.export('jenkins-cd', 'jdl', jdl);
// Create and export a shared instance of the core
// js module
const corejs = require('@jenkins-cd/blueocean-core-js');
jenkinsMods.export('jenkins-cd', 'blueocean-core-js', corejs);
// Load and export the react modules, allowing them to be imported by other bundles.
const react = require('react');
const reactDOM = require('react-dom');
@ -29,9 +34,9 @@ exports.initialize = function (oncomplete) {
// Might want to do some flux fancy-pants stuff for this.
const appRoot = document.getElementsByTagName("head")[0].getAttribute("data-appurl");
Extensions.init({
extensionDataProvider: cb => FetchUtils.fetchJson(`${appRoot}/js-extensions`, body => cb(body.data)),
classMetadataProvider: (type, cb) => FetchUtils.fetchJson(`${appRoot}/rest/classes/${type}/`,cb)
extensionDataProvider: cb => FetchUtils.fetchJson(`${appRoot}/js-extensions`).then(body => cb(body.data)).catch(FetchUtils.consoleError),
classMetadataProvider: (type, cb) => FetchUtils.fetchJson(`${appRoot}/rest/classes/${type}/`).then(cb).catch(FetchUtils.consoleError)
});
oncomplete();
};

View File

@ -144,6 +144,7 @@ function createBundle(jsxFile) {
.namespace(maven.getArtifactId())
.withExternalModuleMapping('@jenkins-cd/js-extensions', 'jenkins-cd:js-extensions')
.withExternalModuleMapping('@jenkins-cd/design-language', 'jenkins-cd:jdl')
.withExternalModuleMapping('@jenkins-cd/blueocean-core-js', 'jenkins-cd:blueocean-core-js')
.withExternalModuleMapping('react', 'react:react')
.withExternalModuleMapping('react-dom', 'react:react-dom')
.withExternalModuleMapping('react-addons-css-transition-group', 'react:react-addons-css-transition-group')