Commit e9faba5a by Johannes Edmeier

add wallboard view

closes #313
parent ce430e29
......@@ -2453,9 +2453,9 @@
}
},
"css-loader": {
"version": "0.28.10",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.10.tgz",
"integrity": "sha512-X1IJteKnW9Llmrd+lJ0f7QZHh9Arf+11S7iRcoT2+riig3BK0QaCaOtubAulMK6Itbo08W6d3l8sW21r+Jhp5Q==",
"version": "0.28.11",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.11.tgz",
"integrity": "sha512-wovHgjAx8ZIMGSL8pTys7edA1ClmzxHeY6n/d97gg5odgsxEgKjULPR0viqyC+FWMCL9sfqoC/QCUBo62tLvPg==",
"dev": true,
"requires": {
"babel-code-frame": "6.26.0",
......@@ -3446,9 +3446,9 @@
}
},
"eslint": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.18.2.tgz",
"integrity": "sha512-qy4i3wODqKMYfz9LUI8N2qYDkHkoieTbiHpMrYUI/WbjhXJQr7lI4VngixTgaG+yHX+NBCv7nW4hA0ShbvaNKw==",
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.0.tgz",
"integrity": "sha512-r83L5CuqaocDvfwdojbz68b6tCUk8KJkqfppO+gmSAQqYCzTr0bCSMu6A6yFCLKG65j5eKcKUw4Cw4Yl4gfWkg==",
"dev": true,
"requires": {
"ajv": "5.5.2",
......@@ -3460,7 +3460,7 @@
"doctrine": "2.1.0",
"eslint-scope": "3.7.1",
"eslint-visitor-keys": "1.0.0",
"espree": "3.5.3",
"espree": "3.5.4",
"esquery": "1.0.0",
"esutils": "2.0.2",
"file-entry-cache": "2.0.0",
......@@ -3482,6 +3482,7 @@
"path-is-inside": "1.0.2",
"pluralize": "7.0.0",
"progress": "2.0.0",
"regexpp": "1.0.1",
"require-uncached": "1.0.3",
"semver": "5.5.0",
"strip-ansi": "4.0.0",
......@@ -3490,12 +3491,28 @@
"text-table": "0.2.0"
},
"dependencies": {
"acorn": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz",
"integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==",
"dev": true
},
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"espree": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
"integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
"dev": true,
"requires": {
"acorn": "5.5.3",
"acorn-jsx": "3.0.1"
}
},
"esprima": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
......@@ -8699,9 +8716,9 @@
}
},
"postcss-loader": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.1.tgz",
"integrity": "sha512-f0J/DWE/hyO9/LH0WHpXkny/ZZ238sSaG3p1SRBtVZnFWUtD7GXIEgHoBg8cnAeRbmEvUxHQptY46zWfwNYj/w==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.2.tgz",
"integrity": "sha512-Hf7gcgJKlJivXZAMprvVq6m6t7lZSkiih4Sa7cOoCd8sEHGMnH/Yc0CbWT2cL1ag+XEKh6nsdpgF6yNHplpa9Q==",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
......@@ -8711,9 +8728,9 @@
},
"dependencies": {
"ajv": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.0.tgz",
"integrity": "sha1-r6wpW7qgFSRJ5SJ0LkVHwa6TKNI=",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.3.0.tgz",
"integrity": "sha1-FlCkERTvAFdMrBC4Ay2PTBSBLac=",
"dev": true,
"requires": {
"fast-deep-equal": "1.0.0",
......@@ -8733,7 +8750,7 @@
"integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==",
"dev": true,
"requires": {
"ajv": "6.2.0",
"ajv": "6.3.0",
"ajv-keywords": "3.1.0"
}
}
......@@ -10475,6 +10492,12 @@
"is-equal-shallow": "0.1.3"
}
},
"regexpp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.0.1.tgz",
"integrity": "sha512-8Ph721maXiOYSLtaDGKVmDn5wdsNaF6Px85qFNeMPQq0r8K5Y10tgP6YuR65Ws35n4DvzFcCxEnRNBIXQunzLw==",
"dev": true
},
"regexpu-core": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
......@@ -10701,6 +10724,11 @@
"resolve-from": "1.0.1"
}
},
"resize-observer-polyfill": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz",
"integrity": "sha512-M2AelyJDVR/oLnToJLtuDJRBBWUGUvvGigj1411hXhAdyFWqMaqHp7TixW3FpiLuVaikIcR1QL+zqoJoZlOgpg=="
},
"resolve": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
......@@ -11985,9 +12013,9 @@
"dev": true
},
"vue-jest": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-2.1.1.tgz",
"integrity": "sha512-JJcErFJ1XhTmW14zBhceZXGuxQoR/DiUQSvS3RDpjSlIpAmqi2rrI/a/6lcyBxm++yZt9TeaAneU0D0zqWtMXg==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-2.2.1.tgz",
"integrity": "sha512-XnJWRFdCkHf+dLaaC0PzIiyQW4hoH7EzqPpPT4/nAmUjx288tsDWL68dbzafcM6c0W9qOPsb2YlFVxDiEd45Zg==",
"dev": true,
"requires": {
"babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
......
......@@ -30,6 +30,7 @@
"moment": "^2.21.0",
"moment-shortformat": "^2.1.0",
"pretty-bytes": "^4.0.2",
"resize-observer-polyfill": "^1.5.0",
"rxjs": "^5.5.7",
"vue": "^2.5.16",
"vue-clickaway": "^2.1.0",
......@@ -50,9 +51,9 @@
"clean-webpack-plugin": "^0.1.19",
"cross-env": "^5.1.4",
"css-hot-loader": "^1.3.8",
"css-loader": "^0.28.10",
"css-loader": "^0.28.11",
"css-mqpacker": "^6.0.2",
"eslint": "^4.18.2",
"eslint": "^4.19.0",
"eslint-loader": "^1.9.0",
"eslint-plugin-html": "^4.0.2",
"eslint-plugin-vue": "^4.3.0",
......@@ -66,11 +67,11 @@
"lodash-webpack-plugin": "^0.11.4",
"node-sass": "^4.8.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"postcss-loader": "^2.1.1",
"postcss-loader": "^2.1.2",
"sass-loader": "^6.0.7",
"style-loader": "^0.20.3",
"url-loader": "^0.6.2",
"vue-jest": "^2.1.1",
"vue-jest": "^2.2.1",
"vue-loader": "^14.2.1",
"vue-svg-loader": "^0.5.0",
"vue-template-compiler": "^2.5.16",
......
......@@ -18,6 +18,24 @@
@import "~bulma/bulma";
@import "~bulma-badge/dist/bulma-badge";
html {
min-height: 100vh;
display: flex;
flex-direction: column;
}
body {
flex-grow: 1;
display: flex;
flex-direction: column;
& > div {
flex-grow: 1;
display: flex;
flex-direction: column;
}
}
.is-breakable {
word-break: break-all;
}
......@@ -50,7 +68,6 @@
}
}
//Loading spinner
.section.is-loading,
.content.is-loading {
......
/*
* Copyright 2014-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ResizeObserver from 'resize-observer-polyfill';
const observers = new WeakMap();
const bind = (el, binding) => {
unbind(el);
const observer = new ResizeObserver(binding.value);
observer.observe(el);
observers.set(el, observer);
};
const unbind = (el) => {
const observer = observers.get(el);
if (observer) {
observer.disconnect();
observers.delete(el);
}
};
export default {
bind,
update(el, binding) {
if (binding.value === binding.oldValue) {
return
}
bind(el, binding)
},
unbind
}
......@@ -18,6 +18,7 @@ import {view as aboutView} from './about';
import {view as applicationView} from './applications';
import instanceViews from './instances';
import {view as journalView} from './journal';
import {view as wallboardView} from './wallboard';
export default router => {
const views = [];
......@@ -49,9 +50,11 @@ export default router => {
views.register(applicationView);
views.register(journalView);
views.register(aboutView);
views.register(wallboardView);
instanceViews.forEach(views.register);
views.sort((a, b) => a.order - b.order);
router.addRoutes([{path: '/', redirect: {name: 'applications'}}]);
return views;
}
\ No newline at end of file
}
/*
* Copyright 2014-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as hm from './hex-mesh'
describe('calcLayout', () => {
it('should calculate optimum layout for 12 in 1594x879', () => {
const result = hm.calcLayout(12, 1594, 879);
expect(result).toEqual({
rows: 3,
cols: 5,
sidelength: 175.8
});
});
it('should calculate optimum layout for 1 in 100x100', () => {
const result = hm.calcLayout(1, 100, 100);
expect(result).toEqual({
rows: 1,
cols: 1,
sidelength: 50
});
});
});
<!--
- Copyright 2014-2018 the original author or authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<div class="hex-mesh" v-on-resize="onResize">
<svg xmlns="http://www.w3.org/2000/svg" :width="meshWidth" :height="meshHeight">
<defs>
<clipPath id="hex-clip">
<polygon :points="hexPath"/>
</clipPath>
</defs>
<template>
<template v-for="row in rows">
<g v-for="col in cols + (row % 2 ? 0 : -1)" :key="`${col}-${row}`"
class="hex" :transform="translate(col, row)"
:class="classForItem(item(col,row))"
@click="click($event,col,row)">
<polygon :points="hexPath"/>
<foreignObject v-if="item(col,row)" x="0" y="0" :width="hexWidth" :height="hexHeight"
style="pointer-events: none">
<slot name="item" v-bind="item(col,row)"/>
</foreignObject>
</g>
</template>
</template>
</svg>
</div>
</template>
<script>
import onResize from '@/directives/on-resize';
const tileCount = (cols, rows) => {
const shorterRows = Math.floor(rows / 2);
return rows * cols - shorterRows;
};
const calcSideLength = (width, height, cols, rows) => {
const fitToWidth = width / cols / Math.sqrt(3);
const fitToHeight = height * 2 / (3 * rows + 1);
return Math.min(fitToWidth, fitToHeight);
};
const calcLayout = (minTileCount, width, height) => {
let cols = 1, rows = 1;
let sideLength = calcSideLength(width, height, cols, rows);
while (minTileCount > tileCount(cols, rows)) {
const sidelengthExtraCol = calcSideLength(width, height, cols + 1, rows);
const sidelengthExtraRow = calcSideLength(width, height, cols, rows + 1);
if (sidelengthExtraCol > sidelengthExtraRow) {
sideLength = sidelengthExtraCol;
cols++;
} else {
sideLength = sidelengthExtraRow;
rows++
}
}
return {
cols,
rows,
sideLength
}
};
export default {
props: {
items: {
type: Array,
default: () => []
},
classForItem: {
type: Function,
default: () => {
}
}
},
directives: {onResize},
data: () => ({
cols: 1,
rows: 1,
sideLength: 1
}),
methods: {
translate(col, row) {
const x = (col - 1) * this.hexWidth + (row % 2 ? 0 : this.hexWidth / 2);
const y = (row - 1) * this.sideLength * 1.5;
return `translate(${x},${y})`;
},
item(col, row) {
const rowOffset = (row - 1) * this.cols - Math.max(Math.floor((row - 1) / 2), 0);
const index = rowOffset + col - 1;
return this.items[index];
},
point(i) {
const innerSideLength = this.sideLength * 0.95;
const marginTop = this.hexHeight / 2;
const marginLeft = this.hexWidth / 2;
return `${ marginLeft + (innerSideLength * Math.cos((1 + i * 2) * Math.PI / 6))},${marginTop + (innerSideLength * Math.sin((1 + i * 2) * Math.PI / 6))}`
},
click(event, col, row) {
const item = this.item(col, row);
if (item) {
this.$emit('click', item, event);
}
},
updateLayout() {
if (this.$el) {
const boundingClientRect = this.$el.getBoundingClientRect();
const layout = calcLayout(this.itemCount, boundingClientRect.width, boundingClientRect.height);
this.cols = layout.cols;
this.rows = layout.rows;
this.sideLength = layout.sideLength;
}
},
onResize(entries) {
for (let e of entries) {
if (e.target === this.$el) {
this.updateLayout();
}
}
}
},
computed: {
itemCount() {
return this.items.length;
},
hexPath() {
return `${this.point(0)} ${this.point(1)} ${this.point(2)} ${this.point(3)} ${this.point(4)} ${this.point(5)}`
},
hexHeight() {
return this.sideLength * 2;
},
hexWidth() {
return this.sideLength * Math.sqrt(3);
},
meshWidth() {
return this.hexWidth * this.cols;
},
meshHeight() {
return this.sideLength * (2 + (this.rows - 1) * 1.5);
}
},
mounted() {
this.updateLayout();
},
watch: {
sideLength(newVal) {
this.$el.style['font-size'] = `${newVal / 9.5}px`;
},
itemCount() {
this.updateLayout();
}
}
};
export {calcLayout};
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.hex-mesh {
background-color: $grey-dark;
width: 100%;
height: 100%;
display: flex;
justify-content: space-around;
align-items: center;
}
.hex {
fill-opacity: 0.05;
stroke-width: 0.5;
stroke-opacity: 0.8;
& polygon {
fill: none;
stroke: $grey;
transition: all $easing 250ms;
}
&.is-primary polygon {
fill: $primary;
fill-opacity: 0.3;
stroke: $primary;
stroke-opacity: 0.95;
stroke-width: 1.5;
}
&.is-danger polygon {
fill: $danger;
fill-opacity: 0.3;
stroke: $danger;
stroke-opacity: 0.95;
stroke-width: 1.5;
}
&.is-warning polygon {
fill: $warning;
fill-opacity: 0.3;
stroke: $warning;
stroke-opacity: 0.95;
stroke-width: 1.5;
}
&.is-selectable:hover {
cursor: pointer;
& polygon {
fill-opacity: 0.85;
stroke-opacity: 1;
}
}
&__body {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
</style>
<!--
- Copyright 2014-2018 the original author or authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<section class="wallboard section">
<hex-mesh :items="applications" :class-for-item="classForApplication" @click="select">
<div class="hex__body application" slot="item" slot-scope="application" :key="application.name">
<div class="application__header application__time-ago is-muted">
<sba-time-ago :date="application.statusTimestamp"/>
</div>
<div class="application__body">
<h1 class="application__name" v-text="application.name"/>
<p class="application__instances is-muted"><span v-text="application.instances.length"/> instances</p>
</div>
<h2 class="application__footer application__version" v-text="application.version"/>
</div>
</hex-mesh>
</section>
</template>
<script>
import hexMesh from './hex-mesh';
const component = {
components: {hexMesh},
props: {
applications: {
type: Array,
default: () => [],
},
error: {
type: null,
default: null
}
},
methods: {
classForApplication(application) {
if (application) {
return 'is-selectable ' + (application.status === 'UP' ? 'is-primary' : (application.status === 'RESTRICTED' ? 'is-warning' : 'is-danger'));
}
return null;
},
select(application) {
this.$router.push(`/applications/${application.name}`);
},
}
};
export default component;
export const view = {
path: '/wallboard',
name: 'wallboard',
handle: 'Wallboard',
order: -100,
component: component
};
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.wallboard {
background-color: $grey-dark;
height: calc(100vh - #{$navbar-height-px});
width: 100%;
& .application {
color: $white-ter;
font-size: 1em;
font-weight: $weight-normal;
line-height: 1;
text-align: center;
overflow: hidden;
display: flex;
flex-direction: column;
&__name {
width: 100%;
padding: 2.5%;
color: $white;
font-size: 2em;
font-weight: $weight-semibold;
line-height: 1.125;
}
&__version {
color: $white-ter;
font-size: 1.25em;
line-height: 1.25;
}
&__header {
width: 90%;
margin-bottom: 0.5em;
}
&__footer {
width: 90%;
margin-top: 0.5em;
}
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment