[#437] Added application panel with events monitor tab.

This commit is contained in:
Fabio Manganiello 2024-12-01 18:30:45 +01:00
parent 82e796e20b
commit 51d0b21162
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
13 changed files with 871 additions and 15 deletions

View file

@ -53,6 +53,8 @@ export default {
return
}
bus.emit('event', event)
if (null in this.handlers) { // lgtm [js/implicit-operand-conversion]
handlers.push(this.handlers[null]) // lgtm [js/implicit-operand-conversion]
}

View file

@ -2,6 +2,9 @@
"alarm": {
"class": "fas fa-stopwatch"
},
"application": {
"class": "fas fa-sliders"
},
"arduino": {
"class": "fas fa-microchip"
},

View file

@ -117,7 +117,7 @@ export default {
computed: {
specialPlugins() {
return ['execute', 'entities', 'file', 'procedures']
return ['execute', 'entities', 'file', 'application', 'procedures']
},
panelNames() {
@ -132,6 +132,7 @@ export default {
let panelNames = Object.keys(this.panels).sort()
panelNames = prepend(panelNames, 'file')
panelNames = prepend(panelNames, 'procedures')
panelNames = prepend(panelNames, 'application')
panelNames = prepend(panelNames, 'execute')
panelNames = prepend(panelNames, 'entities')
return panelNames
@ -151,16 +152,20 @@ export default {
},
displayName(name) {
if (name === 'entities')
return 'Home'
if (name === 'execute')
return 'Execute'
if (name === 'file')
return 'Files'
if (name === 'procedures')
return 'Procedures'
return name
switch (name) {
case 'application':
return 'Application'
case 'entities':
return 'Home'
case 'execute':
return 'Execute'
case 'file':
return 'Files'
case 'procedures':
return 'Procedures'
default:
return name
}
},
setConnected(connected) {

View file

@ -0,0 +1,206 @@
<template>
<a class="event renderer"
:class="{even: index % 2 === 0, odd: index % 2 !== 0, expanded}"
:href="href"
@click.prevent.stop="onClick">
<div class="header">
<div class="col-11 title">
<div class="time-container">
[<span class="time">{{ time }}</span>] &nbsp;
</div>
<div class="type-container">
<span class="type">{{ type }}</span>
</div>
</div>
<div class="col-1 buttons">
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
<DropdownItem text="Raw Event" icon-class="fa fa-file-code" @input="showEditor = true" />
<DropdownItem text="Copy to Clipboard" icon-class="fa fa-copy" @input="copy" />
</Dropdown>
</div>
</div>
<div class="body">
<div class="expanded" @click.stop v-if="expanded">
<div class="row time">
<span class="key"><i class="fas fa-clock"></i> Time</span>
<span class="value scalar">{{ datetime }}</span>
</div>
<div class="row type">
<span class="key"><i class="fas fa-tag"></i> Type</span>
<span class="value scalar">
<a v-if="typeDocHref" :href="typeDocHref" target="_blank">{{ type }}</a>
</span>
</div>
<div class="row id">
<span class="key"><i class="fas fa-id-badge"></i> ID</span>
<span class="value scalar">{{ output?.id }}</span>
</div>
<div class="row origin">
<span class="key"><i class="fas fa-map-marker-alt"></i> Origin</span>
<span class="value scalar">{{ output?.origin }}</span>
</div>
<div class="row args">
<span class="key"><i class="fas fa-cogs"></i> Args</span>
<span class="value object">
<ObjectRenderer :output="output.args" />
</span>
</div>
</div>
</div>
<div class="editor-container" v-if="showEditor">
<FileEditor :file="type.split('.').pop()"
:text="indentedOutput"
:visible="true"
:uppercase="false"
:with-save="false"
content-type="json"
@close="showEditor = false" />
</div>
</a>
</template>
<script>
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
import FileEditor from "@/components/File/EditorModal";
import Mixin from './Mixin'
import ObjectRenderer from './ObjectRenderer'
export default {
mixins: [Mixin],
components: {
Dropdown,
DropdownItem,
FileEditor,
ObjectRenderer,
},
data() {
return {
showEditor: false,
}
},
computed: {
datetime() {
const timestamp = this.output?.timestamp || this.output?._timestamp
if (!timestamp) {
return ''
}
return this.formatDateTime(timestamp)
},
indentedOutput() {
if (!Object.keys(this.output || {})?.length) {
return ''
}
try {
return JSON.stringify(this.output, null, 2)
} catch (err) {
return this.output
}
},
time() {
const timestamp = this.output?.timestamp || this.output?._timestamp
if (!timestamp) {
return ''
}
return this.formatTime(timestamp)
},
href() {
const route = this.$route.fullPath
if (route.match(/&index=\d+/)) {
return route.replace(/&index=\d+/, `&index=${this.index}`)
}
return route + (
this.index != null ? `&index=${this.index}` : ''
)
},
type() {
return this.output?.args?.type
},
typeDocHref() {
if (!this.type?.length) {
return ''
}
const parts = this.type
.replace(/^platypush\.message\.event\./, '')
.split('.')
const module = parts.splice(0, parts.length - 1).join('.')
return `https://docs.platypush.tech/platypush/events/${module}.html#${this.type}`
},
},
methods: {
async copy() {
await this.copyToClipboard(this.indentedOutput)
},
onClick() {
this.expanded = !this.expanded
this.setUrlArgs({index: this.expanded && this.index != null ? this.index : undefined})
},
},
mounted() {
const args = this.getUrlArgs()
if (args.index == this.index?.toString()) {
this.expanded = true
}
},
}
</script>
<style lang="scss" scoped>
@import "./style.scss";
.renderer {
width: 100%;
.header {
.title {
display: flex;
flex-direction: row;
}
.buttons {
display: flex;
justify-content: flex-end;
}
}
.body {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
background: $default-bg-3;
}
.expanded {
width: 100%;
max-width: 800px;
margin: 0.5em auto;
padding: 0 1em;
border-radius: 0.5em;
border: $default-border-2;
}
}
</style>

View file

@ -0,0 +1,67 @@
<script>
import 'highlight.js/lib/common'
import 'highlight.js/styles/night-owl.min.css'
import hljs from "highlight.js"
import Utils from '@/Utils'
export default {
mixins: [Utils],
props: {
output: {
type: [Object, String],
required: true,
},
filter: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
},
data() {
return {
expanded: false,
}
},
computed: {
highlightedText() {
return hljs.highlight(
this.outputString,
{language: this.isJson ? 'json' : 'plaintext'}
).value
},
isJson() {
if (typeof this.output === 'object') {
return true
}
try {
JSON.parse(this.output)
return true
} catch (err) {
return false
}
},
outputString() {
if (!Object.keys(this.output || {})?.length) {
return ''
}
try {
return JSON.stringify(this.output, null, this.expanded ? 2 : 0)
} catch (err) {
return this.output
}
},
},
}
</script>

View file

@ -0,0 +1,69 @@
<template>
<a class="object renderer" :href="$route.fullPath"
@click.prevent.stop="onClick">
<div class="compact" v-if="!expanded">
<i class="toggler fas fa-caret-right" />
<span class="delimiter" v-text="typeof output === 'object' ? '{' : '['" />
<span class="ellipsis">...</span>
<span class="delimiter" v-text="typeof output === 'object' ? '}' : ']'" />
</div>
<div class="expanded" v-else>
<i class="toggler fas fa-caret-down" />
<div class="rows">
<div class="row"
:class="{even: index % 2 === 0, odd: index % 2 !== 0, args: (value instanceof Object) || Array.isArray(value)}"
v-for="(value, key, index) in output"
:key="key">
<span class="key" v-text="key" />
<span class="value scalar" v-text="value" v-if="!(value instanceof Object) && !Array.isArray(value)" />
<span class="value object" v-else>
<ObjectRenderer :output="value" />
</span>
</div>
</div>
</div>
</a>
</template>
<script>
import Mixin from './Mixin'
export default {
name: 'ObjectRenderer',
mixins: [Mixin],
methods: {
onClick() {
this.expanded = !this.expanded
},
},
}
</script>
<style lang="scss" scoped>
@import "./style.scss";
.renderer {
.key {
font-weight: bold;
}
.expanded {
flex-direction: row;
}
.compact {
cursor: pointer;
}
.toggler {
margin: 0.75em 0.75em 0 0;
cursor: pointer;
&:hover {
color: $default-hover-fg;
}
}
}
</style>

View file

@ -0,0 +1,17 @@
<template>
<div class="string renderer">
<pre><code v-html="highlightedText" /></pre>
</div>
</template>
<script>
import Mixin from './Mixin'
export default {
mixins: [Mixin],
}
</script>
<style lang="scss" scoped>
@import "./style.scss";
</style>

View file

@ -0,0 +1,142 @@
.renderer {
--default-bg: #{$background-color};
--even-color: #{$default-bg-2};
--odd-color: inherit;
--hover-bg: #{$hover-bg};
--text-color: #{$default-fg};
--time-color: #{$no-items-color};
--type-color: #{$default-fg-2};
&.dark {
--default-bg: black;
--even-color: #141414;
--odd-color: inherit;
--hover-bg: #333;
--text-color: #fff;
--time-color: #999;
--type-color: #fbf6bb;
}
width: fit-content;
min-width: 100%;
color: var(--text-color);
display: flex;
flex-direction: column;
position: relative;
text-decoration: none;
cursor: initial;
&.even, .even {
background: var(--even-color);
}
&.odd, .odd {
background: var(--odd-color);
}
&:hover {
.expanded, .editor-container {
color: initial !important;
}
}
.header, .expanded {
display: flex;
flex: 1 1 auto;
}
.header {
padding: 0.5em 1em;
cursor: pointer;
@include until($tablet) {
flex-direction: column;
}
&:hover {
background: var(--hover-bg) !important;
}
@include from($tablet) {
align-items: center;
}
}
&.expanded {
.header {
padding: 1em;
font-weight: bold;
border-bottom: 1px solid var(--time-color);
}
}
.expanded {
background: var(--default-bg);
cursor: default;
flex-direction: column;
.rows {
width: calc(100% - 1.35em);
display: flex;
flex-direction: column;
}
.row {
width: 100%;
display: flex;
align-items: center;
padding: 0.25em 0;
margin: 0.25em 0;
@include until($tablet) {
flex-direction: column;
}
&.args {
align-items: flex-start;
flex-direction: column;
}
.key {
@extend .col-s-12, .col-m-4;
}
.value {
display: flex;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
@include from($tablet) {
justify-content: flex-end;
}
&.scalar {
@extend .col-s-12, .col-m-8;
padding-right: 0.5em;
}
&.object {
width: 100%;
}
}
}
}
.time {
color: var(--time-color);
letter-spacing: 0.02em;
}
.type {
color: var(--type-color);
letter-spacing: 0.03em;
}
pre {
margin: 0;
padding: 0;
white-space: nowrap;
overflow: initial;
}
}

View file

@ -13,10 +13,15 @@
<script>
import RestartButton from "@/components/elements/RestartButton"
import StopButton from "@/components/elements/StopButton"
import Utils from '@/Utils'
export default {
name: "Application",
mixins: [Utils],
components: {RestartButton, StopButton},
mounted() {
this.setUrlArgs({ view: 'actions' })
},
}
</script>

View file

@ -0,0 +1,235 @@
<template>
<div class="events-container">
<div class="header">
<div class="filter-container">
<input type="text" v-model="filter" placeholder="Filter events" />
</div>
<div class="btn-container">
<button @click="running = !running" :title="(running ? 'Pause' : 'Start') + ' capturing'">
<i :class="running ? 'fa fa-pause' : 'fa fa-play'" />
</button>
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
<DropdownItem :text="follow ? 'Unfollow' : 'Follow'" icon-class="fa fa-eye" @input="follow = !follow" />
<DropdownItem text="Export Events" icon-class="fa fa-download" @input="download" />
<DropdownItem text="Clear Events" icon-class="fa fa-trash" @input="clear" />
</Dropdown>
</div>
</div>
<div class="body" ref="body">
<EventRenderer v-for="(event, index) in filteredEvents"
:key="index"
:index="index"
:output="event" />
</div>
<div class="footer">
<Loading v-if="running" />
</div>
</div>
</template>
<script>
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
import EventRenderer from "@/components/elements/OutputRenderers/EventRenderer";
import Loading from "@/components/Loading";
import Utils from '@/Utils'
import { bus } from "@/bus";
export default {
mixins: [Utils],
components: {
Dropdown,
DropdownItem,
EventRenderer,
Loading,
},
data() {
return {
filter: '',
follow: true,
output: [],
running: true,
error: null,
}
},
computed: {
filteredEvents() {
const filter = this.filter?.toLowerCase()
return Object.keys(this.serializedEvents)
.filter((i) => {
if (!filter?.length) {
return true
}
return this.serializedEvents[i].includes(filter)
})
.map((i) => this.outputObjects[i])
},
outputString() {
return this.outputStrings.join('\n')
},
outputObjects() {
return this.output.map((item) => {
try {
return JSON.parse(item)
} catch (err) {
return item
}
})
},
outputStrings() {
return this.output.map((item) => {
try {
return JSON.stringify(item)
} catch (err) {
return item
}
})
},
serializedEvents() {
return this.outputObjects.map((item) => {
try {
return JSON.stringify(item).toLowerCase()
} catch (err) {
return item
}
})
},
},
methods: {
clear() {
this.output = []
},
download() {
const blob = new Blob([this.outputString], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `events-${new Date().toISOString()}.json`
a.click()
URL.revokeObjectURL(url)
},
onEvent(msg) {
if (!this.running) {
return
}
this.output.push(msg)
},
},
watch: {
output: {
deep: true,
handler() {
if (!this.follow) {
return
}
this.$nextTick(() => {
this.$refs.body.scrollTop = this.$refs.body.scrollHeight
})
},
},
},
mounted() {
this.setUrlArgs({ view: 'events' })
bus.on('event', this.onEvent)
},
}
</script>
<style lang="scss" scoped>
$header-height: 3.25em;
$header-margin: 0.25em;
$footer-height: 2em;
$btn-container-width: 5em;
.events-container {
width: 100%;
height: 100%;
position: relative;
margin: 0;
background: $background-color;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
.header {
width: 100%;
height: $header-height;
margin-bottom: $header-margin;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background: $default-bg-4;
padding: 0 0.5em;
box-shadow: $border-shadow-bottom;
.filter-container {
width: calc(100% - #{$btn-container-width});
}
.btn-container {
width: $btn-container-width;
display: flex;
flex-direction: row;
justify-content: flex-end;
button {
background: none;
border: none;
padding: 0.5em;
margin-right: 0.5em;
}
}
input[type="text"] {
width: 100%;
max-width: 40em;
}
}
.body {
width: 100%;
height: calc(100% - #{$header-height} - #{$header-margin} - #{$footer-height});
position: relative;
margin: 0 0 $footer-height 0;
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
}
.footer {
width: 100%;
height: $footer-height;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: absolute;
bottom: 0;
font-size: 0.75em;
background: $default-bg-4;
box-shadow: $border-shadow-top;
}
}
</style>

View file

@ -0,0 +1,106 @@
<template>
<div class="app-container">
<div class="tabs">
<Tabs>
<Tab :selected="selectedView === 'actions'"
icon-class="fas fa-cogs"
@input="selectedView = 'actions'">
Actions
</Tab>
<Tab :selected="selectedView === 'events'"
icon-class="fas fa-bolt"
@input="selectedView = 'events'">
Events
</Tab>
</Tabs>
</div>
<div class="content">
<Actions v-if="selectedView === 'actions'" />
<Events v-else-if="selectedView === 'events'" />
</div>
</div>
</template>
<script>
import Actions from "./Actions"
import Events from "./Events"
import Tabs from "@/components/elements/Tabs"
import Tab from "@/components/elements/Tab"
import Utils from '@/Utils'
export default {
mixins: [Utils],
components: {
Actions,
Events,
Tab,
Tabs,
},
data() {
return {
selectedView: 'actions',
}
},
methods: {
setView(view) {
if (!view?.length) {
const urlArgs = this.getUrlArgs()
if (urlArgs.view?.length) {
view = urlArgs.view
}
}
if (!view?.length) {
return
}
this.selectedView = view
},
},
watch: {
$route() {
this.setView()
},
selectedView() {
this.setUrlArgs({ view: this.selectedView })
},
},
created() {
this.setView()
},
}
</script>
<style lang="scss" scoped>
$tabs-height: 3.5em;
.app-container {
width: 100%;
height: 100%;
:deep(.tabs) {
width: 100%;
height: $tabs-height;
display: flex;
align-items: center;
margin-top: -0.1em;
box-shadow: $border-shadow-bottom;
.tab {
height: $tabs-height;
}
}
.content {
height: calc(100vh - #{$tabs-height});
background-color: $background-color;
}
}
</style>

View file

@ -1,7 +1,6 @@
<template>
<div class="settings-container">
<main>
<Application v-if="selectedPanel === 'application'" />
<Users :session-token="sessionToken" :current-user="currentUser"
v-if="selectedPanel === 'users' && currentUser" />
<Tokens :current-user="currentUser"
@ -11,14 +10,13 @@
</template>
<script>
import Application from "@/components/panels/Settings/Application";
import Tokens from "@/components/panels/Settings/Tokens/Index";
import Users from "@/components/panels/Settings/Users";
import Utils from "@/Utils";
export default {
name: "Settings",
components: {Application, Users, Tokens},
components: {Users, Tokens},
mixins: [Utils],
emits: ['change-page'],

View file

@ -102,6 +102,7 @@ export default {
initializeDefaultViews() {
this.plugins.entities = {}
this.plugins.execute = {}
this.plugins.application = {}
this.plugins.file = this.plugins.file || {}
},
},