Major refactoring of frontend code to use JWT
This commit is contained in:
parent
6792a9e93f
commit
aa4b8bd1df
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
};
|
|
@ -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({
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
||||
};
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue