Merge branch 'master' into matdev

This commit is contained in:
Sebastian Frank 2017-09-25 13:08:03 +02:00
commit 8899a291fc
15 changed files with 381 additions and 319 deletions

View File

@ -1,7 +1,7 @@
stages: stages:
- build - build
# - test # - test
- deploy - review
build_ui: build_ui:
image: node image: node
@ -23,62 +23,81 @@ build_ui:
- build - build
- conf - conf
- index.html - index.html
- Dockerfile
- docker-compose.yml
#start_review:
# image: mwienk/docker-lftp
# tags:
# - docker
# dependencies:
# - build_ui
# stage: deploy
# script:
# - cat index.html
# - mkdir _for_upload
# - mv assets build conf index.html _for_upload
# - lftp -c "set sftp:auto-confirm yes; open -u intern_basispanel_ui,$FTP_PASSWORD sftp://ftp.basehosts.de;mkdir -f /$CI_COMMIT_REF_NAME; mirror -v -n -e -R -L _for_upload/ /$CI_COMMIT_REF_NAME"
# environment:
# name: review/$CI_COMMIT_REF_NAME
# url: http://ui.basispanel.de/$CI_COMMIT_REF_NAME/
# on_stop: stop_review
#
#stop_review:
# image: mwienk/docker-lftp
# tags:
# - docker
# stage: deploy
# variables:
# GIT_STRATEGY: none
# script:
# - echo stopping env
# environment:
# name: review/$CI_COMMIT_REF_NAME
# action: stop
# when: manual
#
#
#
#fullsync_review:
# image: mwienk/docker-lftp
# tags:
# - docker
# dependencies:
# - build_ui
# stage: deploy
# script:
# - mkdir _for_upload
# - mv assets build conf index.html _for_upload
# - lftp -c "set sftp:auto-confirm yes; open -u intern_basispanel_ui,$FTP_PASSWORD sftp://ftp.basehosts.de;mkdir -f /$CI_COMMIT_REF_NAME; mirror -v --transfer-all -e -R -L _for_upload/ /$CI_COMMIT_REF_NAME"
# when: manual
start_review: start_review:
image: mwienk/docker-lftp
tags:
- docker
dependencies:
- build_ui
stage: deploy
script:
- cat index.html
- mkdir _for_upload
- mv assets build conf index.html _for_upload
- lftp -c "set sftp:auto-confirm yes; open -u intern_basispanel_ui,$FTP_PASSWORD sftp://ftp.basehosts.de;mkdir -f /$CI_COMMIT_REF_NAME; mirror -v -n -e -R -L _for_upload/ /$CI_COMMIT_REF_NAME"
environment:
name: review/$CI_COMMIT_REF_NAME
url: http://ui.basispanel.de/$CI_COMMIT_REF_NAME/
on_stop: stop_review
stop_review:
image: mwienk/docker-lftp
tags:
- docker
stage: deploy
variables:
GIT_STRATEGY: none
script:
- echo stopping env
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
when: manual
fullsync_review:
image: mwienk/docker-lftp
tags:
- docker
dependencies:
- build_ui
stage: deploy
script:
- mkdir _for_upload
- mv assets build conf index.html _for_upload
- lftp -c "set sftp:auto-confirm yes; open -u intern_basispanel_ui,$FTP_PASSWORD sftp://ftp.basehosts.de;mkdir -f /$CI_COMMIT_REF_NAME; mirror -v --transfer-all -e -R -L _for_upload/ /$CI_COMMIT_REF_NAME"
when: manual
docker_test:
tags: tags:
- shell - shell
dependencies: dependencies:
- build_ui - build_ui
stage: deploy stage: review
script: script:
- mkdir _for_docker - mkdir _for_docker
- mv assets build conf index.html _for_docker - mv assets build conf index.html _for_docker
- docker-compose up -d --build - docker-compose -p ${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME} up -d --build
environment:
name: review/$CI_COMMIT_REF_NAME
url: http://${CI_COMMIT_REF_NAME}.${CI_PROJECT_NAME}.dev.basehosts.de/
on_stop: stop_review
stop_review:
tags:
- shell
variables:
GIT_STRATEGY: none
stage: review
when: manual
script:
- docker-compose -p ${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME} down
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop

View File

@ -1,6 +1,8 @@
[![pipeline status](https://git.basehosts.de:20443/panel/basispanel-ui/badges/master/pipeline.svg)](https://git.basehosts.de:20443/panel/basispanel-ui/commits/master) [![pipeline status](https://git.basehosts.de:20443/panel/basispanel-ui/badges/master/pipeline.svg)](https://git.basehosts.de:20443/panel/basispanel-ui/commits/master)
# basispanel UI # basispanel UI
## Entwicklungsprozess ## Entwicklungsprozess
### Allgemeines ### Allgemeines

View File

@ -4,7 +4,6 @@ services:
frontend: frontend:
build: . build: .
image: ${CI_PROJECT_NAME}:${CI_COMMIT_REF_NAME} image: ${CI_PROJECT_NAME}:${CI_COMMIT_REF_NAME}
container_name: ${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}
restart: always restart: always
networks: networks:
- web - web

View File

@ -8,9 +8,9 @@
<body> <body>
<div id="vue-app"> <div id="vue-app">
<app></app> <div is="app">Lade, bitte warten...</div>
</div> </div>
<script src="build/main.bundle.js"></script> <script src="build/baseui-main.bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -23,7 +23,6 @@
<script> <script>
export default { export default {
name: "MyForm", name: "MyForm",
components: {},
props: { props: {
elements: { elements: {
type: Object, type: Object,

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="Scroll-Table"> <div class="Scroll-Table">
<my-input :props="{type: 'text', placeholder: 'Suche'}" @change="searchChanged" v-model="currentSearch"></my-input> <my-input :props="{type: 'text', placeholder: 'Suche'}" @input="searchChanged" v-model="currentSearch"></my-input>
<my-table :actions="actions" :columns="columns" :rows="rows" <my-table :actions="actions" :columns="columns" :rows="rows"
:currentOrderBy="currentOrderBy" :currentOrderBy="currentOrderBy"
:currentOrderDesc="currentOrderDesc" :currentOrderDesc="currentOrderDesc"
@ -14,20 +14,14 @@
</template> </template>
<script> <script>
import MyTable from 'components/my-table';
import MyInput from 'components/my-input';
import { ObserveVisibility } from 'vue-observe-visibility/dist/vue-observe-visibility'; import { ObserveVisibility } from 'vue-observe-visibility/dist/vue-observe-visibility';
import { debounce } from 'lib/util';
export default { export default {
name: "ScrollTable", name: "ScrollTable",
directives: { directives: {
ObserveVisibility ObserveVisibility
}, },
components: {
MyTable,
MyInput
},
props: { props: {
actions: { actions: {
type: Array, type: Array,
@ -141,11 +135,11 @@ export default {
this.reloadTriggered = true; this.reloadTriggered = true;
this.clear(); this.clear();
}, },
searchChanged(e) { searchChanged: debounce(function(e) {
this.currentSearch = e; this.currentSearch = e;
this.reloadTriggered = true; this.reloadTriggered = true;
this.clear(); this.clear();
} }, 300)
} }
} }
</script> </script>

View File

@ -12,7 +12,7 @@
</a> </a>
<nav class="user_menu"> <nav class="user_menu">
<ul> <ul>
<router-link tag="li" v-for="(item, i) in items" :key="i" v-if="showIf(item.show)" :to="item.to" active-class="active" exact> <router-link tag="li" v-for="(item, i) in items" :key="i" v-if="showIf(item.show)" :to="item.to" active-class="active" exact @click.native="toggleMenu">
<a> <a>
<i :class="['icon', item.icon]"></i> <i :class="['icon', item.icon]"></i>
<div class="title">{{ item.name }}</div> <div class="title">{{ item.name }}</div>

239
src/lib/baseui.js Normal file
View File

@ -0,0 +1,239 @@
import Vue from 'vue';
import Vuex from 'vuex';
import VueRouter from 'vue-router';
import Axios from 'axios';
import JwtDecode from 'jwt-decode';
import App from 'app.vue';
import MyForm from 'components/my-form.vue';
import MyInput from 'components/my-input.vue';
import TextareaInput from 'components/textarea-input.vue';
import MyTable from 'components/my-table.vue';
import ScrollTable from 'components/scroll-table.vue';
Vue.use(VueRouter);
Vue.use(Vuex);
Vue.component('my-form', MyForm);
Vue.component('my-input', MyInput);
Vue.component('textarea-input', TextareaInput);
Vue.component('my-table', MyTable);
Vue.component('scroll-table', ScrollTable);
const globalConf = {
loginEndpoint: 'login',
loginRoute: '/login',
authHeader: 'X-Auth-Token',
responseType: 'json',
initUrl: 'conf/init.json',
el: '#my-app'
}
const Router = new VueRouter();
const Store = new Vuex.Store({
state: {
ui: {},
persist: {
login: {},
authToken: '',
jwt: {},
credentials: {}
}
},
mutations: {
setUI(state, payload) {
state.ui = payload;
},
setCredentials(state, payload) {
state.persist.credentials = payload;
objectToPersist(state.persist, 'persistantStore');
},
setLogin(state, payload) {
state.persist.login = payload.User;
state.persist.authToken = payload.AuthToken;
state.persist.jwt = JwtDecode(payload.AuthToken);
objectToPersist(state.persist, 'persistantStore');
},
clearLogin(state) {
state.persist.login = {};
state.persist.authToken = '';
state.persist.jwt = {};
state.persist.credentials = {};
objectToPersist(state.persist, 'persistantStore');
}
},
actions: {
apiRequest(context, payload) {
let doRequest = () => {
return new Promise((resolve, reject) => {
const addHeader = {};
addHeader[globalConf.authHeader] = context.state.persist.authToken;
Axios({
method: payload.method ? payload.method : 'get',
baseURL: context.state.ui.api.baseURL,
url: payload.endpoint,
params: payload.params,
data: payload.data,
headers: addHeader,
responseType: globalConf.responseType
})
.then(response => {
console.log(response);
resolve(response.data);
})
.catch(error => {
console.dir(error);
reject(error);
})
});
};
if (payload.endpoint != globalConf.loginEndpoint) {
// no jwt check for login call
if (!context.state.persist.jwt.exp) {
return new Promise((resolve, reject) => {
// show login page
Router.push(globalConf.loginRoute);
reject(['not logged in']);
});
}
let now = Math.round(Date.now() / 1000);
if (context.state.persist.jwt.exp < (now + 300)) {
// too old jwt, logout
return new Promise((resolve, reject) => {
context.commit('clearLogin');
Router.push(globalConf.loginRoute);
reject(['jwt too old, logout']);
});
}
if ((context.state.persist.jwt.exp - 60) < now) {
// jwt near expire, get new one
console.log("getting new jwt");
return context.dispatch(globalConf.loginEndpoint, context.state.persist.credentials)
.then(() => {
console.log("LOOOGIIIINNN");
return doRequest();
})
.catch(error => {
throw error;
})
}
}
return doRequest();
},
login(context, payload) {
return new Promise((resolve, reject) => {
context.dispatch('apiRequest', {
method: 'post',
endpoint: globalConf.loginEndpoint,
data: payload
})
.then(data => {
context.commit('setCredentials', payload);
context.commit('setLogin', data);
resolve(data.User);
})
.catch(error => {
if (error.response && error.response.data && error.response.data.error) {
reject(error.response.data.error);
} else {
reject([]);
}
});
});
}
}
});
function objectToPersist(obj, key) {
try {
const serialized = JSON.stringify(obj);
localStorage.setItem(key ? key : 'persistant', serialized);
} catch (error) {
console.error(error);
}
}
function persistToObject(key) {
try {
const serialized = localStorage.getItem(key ? key : 'persistant');
if (serialized === null) {
return undefined;
}
return JSON.parse(serialized);
} catch (error) {
console.log(error);
return undefined;
}
}
// get store persist part from localStorage
let persist = persistToObject('persistantStore');
if (persist) {
Store.state.persist = persist;
}
export default {
Vue,
Router,
Store,
Axios,
InitApp(config) { // config: {initUrl, views, el}
Object.assign(globalConf, config);
Axios.get(globalConf.initUrl)
.then(results => {
// set navigation
if (!Array.isArray(results.data.routes)) {
alert('invalid data in init.json, no routes');
return;
}
// set ui config in store
Store.commit("setUI", results.data.ui);
// add routes
let routes = [];
let rIdx = 0;
results.data.routes.forEach(({ name, to, content, data }) => routes.push({
name: name,
path: to,
meta: {
title: name
},
component: {
template: '<div id="' + name + rIdx + '">' + content + '</div>',
components: globalConf.views,
data: function (data) {
if (typeof data != 'object') {
return () => { return {}; };
}
return () => { return data; };
}(data)
}
}));
Router.addRoutes(routes);
Router.beforeEach((to, from, next) => {
document.title = (to.meta && to.meta.title) ? results.data.ui.title + ': ' + to.meta.title : results.data.title;
next();
});
// load app, when init finishs
new Vue({
el: globalConf.el,
render: h => h(App),
router: Router,
store: Store
});
})
.catch(error => {
alert('error loading: ' + error.message);
});
}
};

36
src/lib/util.js Normal file
View File

@ -0,0 +1,36 @@
const now = Date.now || function() {
return new Date().getTime();
};
export function debounce(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
var last = now() - timestamp;
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = now();
var callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
}

View File

@ -1,218 +1,10 @@
import Vue from 'vue'; import BaseUI from './lib/baseui.js';
import Vuex from 'vuex'; import Views from './views/views.js';
import VueRouter from 'vue-router';
import Axios from 'axios';
import JwtDecode from 'jwt-decode';
import App from './app.vue';
// import Views from './views/views.js';
Vue.use(VueRouter);
Vue.use(Vuex);
const router = new VueRouter();
const store = new Vuex.Store({
state: {
ui: {},
persist: {
login: {},
authToken: '',
jwt: {},
credentials: {}
}
},
mutations: {
setUI(state, payload) {
state.ui = payload;
},
setCredentials(state, payload) {
state.persist.credentials = {
username: payload.username,
password: payload.password
};
objectToPersist(state.persist, 'persistantStore');
},
setLogin(state, payload) {
state.persist.login = payload.User;
state.persist.authToken = payload.AuthToken;
state.persist.jwt = JwtDecode(payload.AuthToken);
objectToPersist(state.persist, 'persistantStore');
},
clearLogin(state) {
state.persist.login = {};
state.persist.authToken = '';
state.persist.jwt = {};
state.persist.credentials = {};
objectToPersist(state.persist, 'persistantStore');
}
},
actions: {
apiRequest(context, payload) {
let doRequest = () => {
return new Promise((resolve, reject) => {
Axios({
method: payload.method ? payload.method : 'get',
baseURL: context.state.ui.api.baseURL,
url: payload.endpoint,
params: payload.params,
data: payload.data,
headers: {
'X-Auth-Token': context.state.persist.authToken
}
})
.then(response => {
console.log(response);
resolve(response.data);
})
.catch(error => {
console.dir(error);
reject(error);
})
});
};
if (payload.endpoint != 'login') {
// no jwt check for login call
// empty username = not logged in
if (!context.state.persist.credentials.username) {
return new Promise((resolve, reject) => {
// show login page
router.push('/login');
reject(['not logged in']);
});
}
let now = Math.round(Date.now() / 1000);
if (context.state.persist.jwt.exp < (now + 300)) {
// too old jwt, logout
return new Promise((resolve, reject) => {
context.commit('clearLogin');
router.push('/login');
reject(['jwt too old, logout']);
});
}
if ((context.state.persist.jwt.exp - 60) < now) {
// jwt near expire, get new one
console.log("getting new jwt");
return context.dispatch('login', context.state.persist.credentials)
.then(() => {
console.log("LOOOGIIIINNN");
return doRequest();
})
.catch(error => {
throw error;
})
}
}
return doRequest();
},
login(context, payload) {
return new Promise((resolve, reject) => {
context.dispatch('apiRequest', {
method: 'post',
endpoint: 'login',
data: payload
})
.then(data => {
context.commit('setCredentials', payload);
context.commit('setLogin', data);
resolve(data.User);
})
.catch(error => {
if (error.response && error.response.data && error.response.data.error) {
reject(error.response.data.error);
} else {
reject([]);
}
});
});
}
}
});
function objectToPersist(obj, key) {
try {
const serialized = JSON.stringify(obj);
localStorage.setItem(key ? key : 'persistant', serialized);
} catch (error) {
console.error(error);
}
}
function persistToObject(key) {
try {
const serialized = localStorage.getItem(key ? key : 'persistant');
if (serialized === null) {
return undefined;
}
return JSON.parse(serialized);
} catch (error) {
console.log(error);
return undefined;
}
}
// get store persist part from localStorage
let persist = persistToObject('persistantStore');
if (persist) {
store.state.persist = persist;
}
// load views // load init
import(/* webpackChunkName: "views" */ './views/views.js').then(Views => { BaseUI.InitApp({
// load init el: '#vue-app',
Axios.get('conf/init.json') initUrl: 'conf/init.json',
.then(results => { views: Views
// set navigation
if (!Array.isArray(results.data.routes)) {
alert('invalid data in init.json, no routes');
return;
}
// set ui config in store
store.commit("setUI", results.data.ui);
// add routes
let routes = [];
let rIdx = 0;
results.data.routes.forEach(({name, to, content, data}) => routes.push({
name: name,
path: to,
meta: {
title: name
},
component: {
template: '<div id="' + name + rIdx + '">' + content + '</div>',
components: Views.default,
data: function(data) {
if (typeof data != 'object') {
return () => { return {}; };
}
return () => { return data; };
}(data)
}
}));
router.addRoutes(routes);
router.beforeEach((to, from, next) => {
document.title = (to.meta && to.meta.title) ? results.data.ui.title + ': ' + to.meta.title : results.data.title;
next();
});
// load app, when init finishs
new Vue({
el: '#vue-app',
render: h => h(App),
router,
store
});
})
.catch(error => {
alert('error loading: ' + error.message);
});
}); });

View File

@ -17,22 +17,15 @@
</template> </template>
<script> <script>
import MyForm from "components/my-form";
import MyInput from "components/my-input";
export default { export default {
name: "LoginForm", name: "LoginForm",
components: {
MyForm,
MyInput
},
data() { data() {
return { return {
username: "", username: "",
password: "", password: "",
elements: { elements: {
username: { username: {
element: MyInput, element: "my-input",
icon: "icon-user", icon: "icon-user",
required: true, required: true,
requiredMessage: "Der Benutzername wird benötigt!", requiredMessage: "Der Benutzername wird benötigt!",
@ -42,7 +35,7 @@ export default {
} }
}, },
password: { password: {
element: MyInput, element: "my-input",
icon: "icon-key", icon: "icon-key",
required: true, required: true,
requiredMessage: "Das Passwort wird benötigt!", requiredMessage: "Das Passwort wird benötigt!",

View File

@ -105,17 +105,8 @@
</template> </template>
<script> <script>
import MyForm from "components/my-form";
import MyInput from "components/my-input";
import TextareaInput from "components/textarea-input";
export default { export default {
name: "FormDemo", name: "FormDemo",
components: {
MyForm,
MyInput,
TextareaInput
},
data() { data() {
return { return {
username: "", username: "",
@ -124,7 +115,7 @@ export default {
title: { title: {
label: "Titel", label: "Titel",
required: true, required: true,
element: MyInput, element: "my-input",
props: { props: {
type: "text", type: "text",
placeholder: "Titel hier eintragen" placeholder: "Titel hier eintragen"
@ -134,7 +125,7 @@ export default {
label: "Permalink", label: "Permalink",
description: "Hier steht ein Beschreibungstext für etwas begriffsstutzige Menschen, die eine Beschreibung zum Befüllen des Feldes brauchen.", description: "Hier steht ein Beschreibungstext für etwas begriffsstutzige Menschen, die eine Beschreibung zum Befüllen des Feldes brauchen.",
required: true, required: true,
element: MyInput, element: "my-input",
props: { props: {
type: "text", type: "text",
placeholder: "Permalink" placeholder: "Permalink"
@ -144,7 +135,7 @@ export default {
label: "Inhalt", label: "Inhalt",
description: "Hier steht ein Beschreibungstext für etwas begriffsstutzige Menschen, die eine Beschreibung zum Befüllen des Feldes brauchen.", description: "Hier steht ein Beschreibungstext für etwas begriffsstutzige Menschen, die eine Beschreibung zum Befüllen des Feldes brauchen.",
required: true, required: true,
element: TextareaInput, element: "textarea-input",
props: { props: {
placeholder: "Inhalt" placeholder: "Inhalt"
} }
@ -152,7 +143,7 @@ export default {
public: { public: {
label: "Is Public", label: "Is Public",
required: true, required: true,
element: MyInput, element: "my-input",
props: { props: {
type: "checkbox" type: "checkbox"
} }

View File

@ -6,13 +6,8 @@
</template> </template>
<script> <script>
import ScrollTable from 'components/scroll-table.vue';
export default { export default {
name: "Domainlist", name: "Domainlist",
components: {
ScrollTable
},
data() { data() {
return { return {
actions: [{ actions: [{

View File

@ -6,13 +6,8 @@
</template> </template>
<script> <script>
import ScrollTable from 'components/scroll-table.vue';
export default { export default {
name: "Userlist", name: "Userlist",
components: {
ScrollTable
},
data() { data() {
return { return {
actions: [{ actions: [{

View File

@ -30,17 +30,25 @@ if (env === 'production') {
// Main Settings config // Main Settings config
module.exports = { module.exports = {
entry: [ entry: {
'intersection-observer', lib: [
'babel-polyfill', 'intersection-observer',
entryPoint 'babel-polyfill',
], './src/lib/baseui.js'
],
main: [
'intersection-observer',
'babel-polyfill',
entryPoint
]
},
devtool: 'source-map', devtool: 'source-map',
output: { output: {
path: exportPath, path: exportPath,
publicPath: 'build/', publicPath: 'build/',
filename: '[name].bundle.js', filename: 'baseui-[name].bundle.js',
chunkFilename: '[name].bundle.js' library: 'baseUI',
libraryTarget: 'umd'
}, },
module: { module: {
loaders: [{ loaders: [{