Commit 576f62f9 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '4-build-docker-image' into 'master'

Resolve "Build docker image"

Closes #4

See merge request genesys-pgr/genesys-ui!2
parents 3ee15e7d 6abf2517
variables:
IMAGE_VERSION: "1.9-SNAPSHOT"
IMAGE_VERSION: "0.1-SNAPSHOT"
DOCKER_HOST: "genesys1.swarm.genesys-pgr.org"
ARTIFACTS: "target/app"
......@@ -100,9 +100,10 @@ deploy for review:
when: manual
except:
- master
dependencies:
environment:
name: catalog/$CI_COMMIT_REF_SLUG
url: https://${CI_ENVIRONMENT_SLUG}.review.genesys-pgr.org
name: genesys/$CI_COMMIT_REF_SLUG
url: https://review.genesys-pgr.org/g/${CI_ENVIRONMENT_SLUG}
on_stop: remove review instance
before_script:
# Address the swarm
......@@ -123,10 +124,12 @@ deploy for review:
else
export IMAGE_TAG="${IMAGE_VERSION}-${CI_COMMIT_REF_SLUG}";
fi
- export CATALOG_API_URL=https://api.catalog.demo.genesys-pgr.org
- export CATALOG_FRONTEND_HOSTNAME=${CI_ENVIRONMENT_SLUG}.review.genesys-pgr.org
- echo "Deploying ${CI_REGISTRY_IMAGE}:${IMAGE_TAG} for review as ${CATALOG_FRONTEND_HOSTNAME}"
- echo "Settings CLIENT_ID=${CATALOG_CLIENT_ID} CLIENT_SECRET=${CATALOG_CLIENT_SECRET}"
- export FRONTEND_HOSTNAME=review.genesys-pgr.org
- export FRONTEND_PATH=/g/${CI_ENVIRONMENT_SLUG}
- export FRONTEND_URL=https://${FRONTEND_HOSTNAME}${FRONTEND_PATH}
- export API_URL=https://sandbox.genesys-pgr.org
- echo "Deploying ${CI_REGISTRY_IMAGE}:${IMAGE_TAG} for review as ${FRONTEND_URL}"
# - echo "Settings CLIENT_ID=${CLIENT_ID} CLIENT_SECRET=${CLIENT_SECRET}"
- envsubst < docker/review-compose-template.yml > review-composed.yml
- cat review-composed.yml
- ${DOCKER_CMD} stack rm ${CI_ENVIRONMENT_SLUG} || true
......@@ -140,12 +143,13 @@ remove review instance:
- master
variables:
GIT_STRATEGY: none
dependencies:
environment:
name: catalog/$CI_COMMIT_REF_SLUG
url: https://${CI_ENVIRONMENT_SLUG}.review.genesys-pgr.org
name: genesys/$CI_COMMIT_REF_SLUG
url: https://review.genesys-pgr.org/g/${CI_ENVIRONMENT_SLUG}
action: stop
before_script:
- echo Removing review https\://${CI_ENVIRONMENT_SLUG}.review.genesys\-pgr.org
- echo Removing review https://review.genesys\-pgr.org/g/${CI_ENVIRONMENT_SLUG}
# Address the swarm
- export DOCKER_HOST=swarm.genesys-pgr.org
# Configuration
......@@ -163,6 +167,9 @@ remove review instance:
deploy to sandbox:
stage: deploy
image: docker:latest
variables:
GIT_STRATEGY: none
dependencies:
only:
- master
before_script:
......@@ -178,7 +185,7 @@ deploy to sandbox:
- DOCKER_CMD=docker
script:
# Actions
- ${DOCKER_CMD} service update --image ${CI_REGISTRY_IMAGE}:${IMAGE_VERSION} catalog-sandbox_frontend
- ${DOCKER_CMD} service update --image ${CI_REGISTRY_IMAGE}:${IMAGE_VERSION} genesysuidemo
environment:
name: sandbox
url: https://catalog.demo.genesys-pgr.org
url: https://genesys.demo.genesys-pgr.org
......@@ -17,6 +17,7 @@ module.exports = {
output: {
filename: '[name].js',
chunkFilename: '[name].js',
publicPath: '',
path: path.join(process.cwd(), 'target/app/server')
},
......
......@@ -102,7 +102,7 @@ module.exports = {
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
path: path.join(process.cwd(), 'target/app/assets'),
publicPath: '/'
publicPath: ''
},
resolve: {
......@@ -232,7 +232,7 @@ module.exports = {
template: './entrypoints/index.html',
chunksSortMode: sortedChunks(['vendor', 'common', 'catalog']),
chunks: ['vendor', 'common', 'catalog'],
favicon: './favicon.ico'
favicon: 'favicon.ico'
}),
// Defer/Async scripts
......@@ -251,7 +251,7 @@ module.exports = {
template: './server/ssr-template.html',
chunksSortMode: sortedChunks(['vendor', 'common', 'catalog']),
chunks: ['vendor', 'common', 'catalog'],
favicon: './favicon.ico'
favicon: 'favicon.ico'
}),
......
......@@ -12,10 +12,10 @@ RUN apk add --no-cache gettext
RUN npm install pm2 -g
# Entrypoint and template
COPY ssr.sh catalogui-pm2.yml /
COPY ssr.sh genesys-pm2.yml /
ENV USER=nobody \
APP_PATH=/var/www/genesys-catalog-ui
APP_PATH=/var/www/genesys-ui
# Application
COPY app ${APP_PATH}/
......
apps:
- script: server.js
args: >-
--api-url=${CATALOG_API_URL}
--frontend-path=${FRONTEND_PATH}
--api-url=${API_URL}
--api-timeout=${API_TIMEOUT}
--client-id=${CLIENT_ID} --client-secret=${CLIENT_SECRET}
--client-id=${CLIENT_ID}
--client-secret=${CLIENT_SECRET}
--geonames-username=${GEONAMES_USERNAME}
--genesys-url=${GENESYS_URL}
--genesys-client-id=${GENESYS_CLIENT_ID}
--genesys-client-secret=${GENESYS_CLIENT_SECRET}
name: catalogui
name: genesysui
exec_mode: cluster
instances: 3
log_type: raw
cwd: /var/www/genesys-catalog-ui/server
cwd: /var/www/genesys-ui/server
env:
NODE_ENV: production
......@@ -4,9 +4,10 @@ services:
frontend:
image: ${CI_REGISTRY_IMAGE}:${IMAGE_TAG}
environment:
- "CLIENT_ID=${CATALOG_CLIENT_ID}"
- "CLIENT_SECRET=${CATALOG_CLIENT_SECRET}"
- "CATALOG_API_URL=${CATALOG_API_URL}"
- "FRONTEND_PATH=${FRONTEND_PATH}"
- "API_URL=${API_URL}"
- "CLIENT_ID=${CLIENT_ID}"
- "CLIENT_SECRET=${CLIENT_SECRET}"
- "GEONAMES_USERNAME=${GEONAMES_USERNAME}"
- "SSR=true"
- "ALLOW_ROBOTS=false"
......@@ -17,7 +18,7 @@ services:
labels:
- traefik.port=3000
- traefik.docker.network=traefik-net
- traefik.frontend.rule=Host:${CATALOG_FRONTEND_HOSTNAME}
- "traefik.frontend.rule=Host:${FRONTEND_HOSTNAME};PathPrefixStrip:${FRONTEND_PATH}"
resources:
limits:
memory: 300m
......
#!/bin/sh
echo "Running Genesys Catalog UI from `pwd` with CLIENT_ID=${CLIENT_ID} and API at ${CATALOG_API_URL}"
echo "Running Genesys UI from `pwd` with CLIENT_ID=${CLIENT_ID} and API at ${API_URL}"
echo "Starting on TCP port ${PORT} (default 3000)."
envsubst < /catalogui-pm2.yml > catalogui-pm2.yml
envsubst < /genesys-pm2.yml > genesys-pm2.yml
exec pm2-docker start catalogui-pm2.yml
exec pm2-docker start genesys-pm2.yml
......@@ -26,8 +26,11 @@ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const initialLanguage = window.initialLanguage;
const initialI18nStore = window.initialI18nStore;
const detectedLang = initialLanguage ? initialLanguage : detectLocaleFromPath(window.location.pathname, 0);
const historyOptions = { basename: detectedLang !== 'en' ? `/${detectedLang}` : '' };
// document.baseURI is full URI, we take everything but the trailing slash (for "/" it is "", for "/aa/" or "/aa" it is "/aa")
const virtualPath = document.baseURI.replace(/^(https?:\/\/[^\/]+)?(.*)\/$/, '$2');
const detectedLang = initialLanguage ? initialLanguage : detectLocaleFromPath(virtualPath, window.location.pathname, 0);
const historyOptions = { basename: detectedLang !== 'en' ? `${virtualPath}/${detectedLang}` : `${virtualPath}` };
const history = createHistory(historyOptions);
const initialState = __PRELOADED_STATE__ === undefined ? {} : __PRELOADED_STATE__;
......
......@@ -3,6 +3,7 @@
<head>
<title>Genesys PGR</title>
<base href="/" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes" />
<meta name="author" content="Genesys Team, helpdesk@genesys-pgr.org" />
......
......@@ -8,8 +8,11 @@ const LanguageDetector = require('i18next-browser-languagedetector');
const lngDetector = new LanguageDetector();
lngDetector.addDetector(langDetector);
// copied from client.tsx
const virtualPath = document.baseURI.replace(/^(https?:\/\/[^\/]+)?(.*)\/$/, '$2');
const backend = {
loadPath: '/locales/{{lng}}/{{ns}}.json',
loadPath: `${virtualPath}/locales/{{lng}}/{{ns}}.json`,
// addPath: '/locales/{{lng}}/{{ns}}.missing.json',
};
......@@ -20,6 +23,12 @@ optionsBase[backendKey] = backend;
const i18nClient = i18n
.use(XHR)
.use(lngDetector)
.init(optionsBase);
.init({
detection: {
order: ['catalogLangDetector'],
lookupFromPathIndex: 0,
},
...optionsBase
});
export default i18nClient;
......@@ -17,6 +17,12 @@ optionsBase[backendKey] = backend;
const i18nServer = i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init(optionsBase);
.init({
detection: {
order: ['path'],
lookupFromPathIndex: 0,
},
...optionsBase
});
export default i18nServer;
......@@ -6,9 +6,12 @@ export default {
lookup: function lookup(options) {
let found;
if (typeof window !== 'undefined') {
found = detectLocaleFromPath(window.location.pathname, options.lookupFromPathIndex);
// copied from client.tsx
const virtualPath = document.baseURI.replace(/^(https?:\/\/[^\/]+)?(.*)\/$/, '$2');
found = detectLocaleFromPath(virtualPath, window.location.pathname, options.lookupFromPathIndex);
}
return found;
},
};
const optionsBase = {
detection: {
order: ['catalogLangDetector'],
lookupFromPathIndex: 0,
},
fallbackLng: 'en',
preload: ['en'],
load: 'languageOnly', // we only provide en, de -> no region specific locals like en-US, de-DE
......
......@@ -4136,8 +4136,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
......@@ -4158,14 +4157,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
......@@ -4180,20 +4177,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
......@@ -4310,8 +4304,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
......@@ -4323,7 +4316,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
......@@ -4338,7 +4330,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
......@@ -4346,8 +4337,7 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
......@@ -4371,7 +4361,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
......@@ -4452,8 +4441,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
......@@ -4586,7 +4574,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
......@@ -4606,7 +4593,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
......
......@@ -3,7 +3,7 @@ import * as minimist from 'minimist';
// console.log(process.argv);
const argv = minimist(process.argv.slice(2), {
string: [
'--genesys-url', '--genesys-client-id', '--genesys-client-secret',
'--api-url', '--client-id', '--client-secret', '--frontend-path',
],
});
console.dir(argv);
......@@ -13,14 +13,19 @@ console.dir(argv);
*/
const config = {
// Timeout (ms) for proxied calls to the API
frontendPath: argv['frontend-path'] || '',
// Backend
apiTimeout: +(argv['api-timeout'] || 2000),
// Genesys PGR
genesysUrl: argv['genesys-url'] || 'localhost:8080',
genesysClientId: argv['genesys-client-id'] || 'my-trusted-client',
genesysClientSecret: argv['genesys-client-secret'] || 'my-secret-client',
apiUrl: argv['api-url'] || 'localhost:8080',
clientId: argv['client-id'] || 'defaultclient@localhost',
clientSecret: argv['client-secret'] || 'changeme',
};
if (config.frontendPath.endsWith('/')) {
console.log(`FRONTEND_PATH "${config.frontendPath}" must not end with trailing /`);
config.frontendPath = config.frontendPath.substring(0, config.frontendPath.lastIndexOf('/'));
}
console.log('Catalog config', config);
export default config;
import languages from 'data/Languages';
export default function detectLocaleFromPath(path, index) {
export default function detectLocaleFromPath(virtualPath: string, path: string, index: number) {
if (virtualPath && path.startsWith(virtualPath)) {
path = path.substring(virtualPath.length);
}
let found = 'en';
const language = path.match(/\/([a-zA-Z-]*)/g);
if (language instanceof Array) {
if (typeof index === 'number') {
const foundLang = language[index].replace('/', '');
languages.some((item, i, arr) => {
if (item.short === foundLang) {
found = foundLang;
return true;
}
});
}
const foundLang = language[index].replace('/', '');
languages.some((item, i, arr) => {
if (item.short === foundLang) {
found = foundLang;
return true;
}
});
}
return found;
}
import * as proxy from 'express-http-proxy';
import config from '../config';
const genesysProxy = proxy(config.genesysUrl, {
const genesysProxy = proxy(config.apiUrl, {
parseReqBody: false,
timeout: config.apiTimeout * 2, // double normal timeout
timeout: config.apiTimeout,
filter: (req, res) => {
if (req.url.startsWith('/oauth/') || req.url.startsWith('/token') || req.url.startsWith('/google')) {
......@@ -16,11 +16,11 @@ const genesysProxy = proxy(config.genesysUrl, {
},
proxyReqPathResolver: (req) => {
let path = req.url;
let path = req.url; // .replace(config.frontendPath, '');
if (path.startsWith('/oauth/token')) {
const grantType = req.query.grant_type;
if (grantType === 'client_credentials' || grantType === 'password') {
path = `${path}&client_id=${config.genesysClientId}&client_secret=${config.genesysClientSecret}`;
path = `${path}&client_id=${config.clientId}&client_secret=${config.clientSecret}`;
// remove all headers from request
req.headers = {};
}
......@@ -34,7 +34,7 @@ const genesysProxy = proxy(config.genesysUrl, {
delete req.headers.cookie;
}
console.log(`HTTP proxy to ${config.genesysUrl}${path}`);
console.log(`HTTP proxy to ${config.apiUrl}${path}`);
return path;
},
});
......
......@@ -24,6 +24,7 @@ import renderRoutes from 'ui/renderRoutes';
import fetchComponentData from './fetchComponentData';
import detectLocaleFromPath from './detectLocaleFromPath';
import getDir from './detectDirection';
import config from '../config';
const prerenderer = (html) => (req, res) => {
console.log('Init prerenderer, request url:', req.url);
......@@ -31,12 +32,13 @@ const prerenderer = (html) => (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'X-Requested-With');
const store = createStore(rootReducer, {} as any, applyMiddleware(thunk, routerMiddleware(createMemoryHistory)));
console.log(`Processing request`, req._parsedOriginalUrl);
const pathname = req._parsedOriginalUrl.pathname;
const search = req._parsedOriginalUrl.search;
const context = {};
function setLocale() {
const locale = detectLocaleFromPath(pathname, 0);
const locale = detectLocaleFromPath(config.frontendPath, pathname, 0);
console.log('Detected locale for SSR is', locale);
req.i18n.changeLanguage(locale);
}
......@@ -47,17 +49,22 @@ const prerenderer = (html) => (req, res) => {
const generateClassName = createGenerateClassName();
const initialLanguage = req.i18n.language;
const initialI18nStore = {};
req.i18n.languages.forEach((l) => {
initialI18nStore[l] = req.i18n.services.resourceStore.data[l];
});
const basename = initialLanguage !== 'en' ? `/${initialLanguage}` : '';
initialI18nStore[initialLanguage] = req.i18n.services.resourceStore.data[initialLanguage];
if (initialLanguage !== 'en') {
// So that we have missing translations
initialI18nStore['en'] = req.i18n.services.resourceStore.data['en'];
}
const basename = initialLanguage !== 'en' ? `${config.frontendPath}/${initialLanguage}` : `${config.frontendPath}`;
const pathWithoutLang = req.url.substr(initialLanguage !== 'en' ? 3 : 0, req.url.length);
console.log(`<StaticRouter location="${pathWithoutLang}" basename="${basename}"`);
const InitialView = (
<ReduxProvider store={ store }>
<JssProvider generateClassName={ generateClassName } registry={ sheets }>
<MuiThemeProvider theme={ theme } sheetsManager={ new Map() }>
<I18nextProvider i18n={ req.i18n }>
<StaticRouter location={ req.url } context={ context } basename={ basename }>
<StaticRouter location={ pathWithoutLang } context={ context } basename={ basename }>
{ renderRoutes(routes) }
</StaticRouter>
</I18nextProvider>
......@@ -76,52 +83,57 @@ const prerenderer = (html) => (req, res) => {
console.log('initialState:', initialState);
}
return html
.replace('SERVER_RENDERED_CSS', sheets.toString())
.replace('SERVER_RENDERED_STATE', serialize(initialState, {isJSON: true}))
.replace('SERVER_RENDERED_HTML', componentHTML)
.replace('SERVER_RENDERED_TITLE', titleState)
.replace('INITIAL_I18N_STORE', serialize(initialI18nStore, {isJSON: true}))
.replace('SERVER_DETECTED_LANGUAGE', serialize(initialLanguage, {isJSON: false}))
.replace('HTML_DIR', serialize(getDir(initialLanguage), {isJSON: false}))
.replace('HTML_LANG', serialize(initialLanguage, {isJSON: false}));
return html.replace(/SERVER_RENDERED_(CSS|STATE|HTML|TITLE|I18NSTORE|DIR|LANG)/g, (match, x) => {
console.log(`Injecting ${match}`);
switch (match) {
case 'SERVER_RENDERED_CSS': return sheets.toString();
case 'SERVER_RENDERED_STATE': return serialize(initialState, {isJSON: true});
case 'SERVER_RENDERED_HTML': return componentHTML;
case 'SERVER_RENDERED_TITLE': return titleState;
case 'SERVER_RENDERED_I18NSTORE': return serialize(initialI18nStore, {isJSON: true});
case 'SERVER_RENDERED_DIR': return serialize(getDir(initialLanguage), {isJSON: false});
case 'SERVER_RENDERED_LANG': return serialize(initialLanguage, {isJSON: false});
default: console.log(`Unrecognized variable in ssr-template.html: ${match}`); return '';
}
});
}
setLocale();
// checkAuthToken(req, res, store.dispatch).then(() => {
const language = req.i18n.language;
const pathWithoutLang = pathname.substr(language !== 'en' ? 3 : 0, pathname.length);
const branch = matchRoutes(routes, pathWithoutLang);
fetchComponentData(store.dispatch, branch, search)
.then(() => { return renderView();
}).then((html) => {
const serverRenderTime = `${Date.now() - startTime}ms`;
console.log('Server render time:', startTime, Date.now(), serverRenderTime);
const r = html.replace('__SERVER_RENDER_TIME__', serverRenderTime);
const keyStatus = 'status';
const keyUrl = 'url';
const status = context[keyStatus];
const url = context[keyUrl];
if (status && status === 404) {
res.status(404);
}
if (url) {
if (url === '/login') {
res.status(401);
} else {
res.status(302);
}
const language = req.i18n.language;
const pathWithoutLang = pathname.substr(language !== 'en' ? 3 : 0, pathname.length);
console.log(`Rendering ${pathWithoutLang} for ${pathname}`);
const branch = matchRoutes(routes, pathWithoutLang);