Merge pull request #242 from meteyou/feature/history

Feature/history
This commit is contained in:
Stefan Dej 2021-03-27 13:19:47 +01:00 committed by GitHub
commit fcf5d38dce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1564 additions and 56 deletions

15
package-lock.json generated
View File

@ -21,6 +21,7 @@
"vue-github-api": "^0.1.7",
"vue-headful": "^2.1.0",
"vue-i18n": "^8.22.4",
"vue-load-image": "^0.1.12",
"vue-observe-visibility": "^1.0.0",
"vue-plotly": "^1.1.0",
"vue-prism-editor": "^1.2.2",
@ -17409,6 +17410,14 @@
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.24.2.tgz",
"integrity": "sha512-+TkAPBQw4Cp2bQrSPtPNkhET7XcWYjjDt1UjWYQs+xbT41q5OAl1I3IZyhg0drjn1nlC1K0f8sLVB/nshUcF1Q=="
},
"node_modules/vue-load-image": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/vue-load-image/-/vue-load-image-0.1.12.tgz",
"integrity": "sha512-F2OwNP0hB0OG1wOy/r9SJ5oFiKvpgS1jluMkHtr3VFtbrhQPvuL53wNxatjkONn3Ciw6Ui54hBCgGBLAwwkZYw==",
"peerDependencies": {
"vue": "^2.0.0"
}
},
"node_modules/vue-loader": {
"version": "15.9.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.3.tgz",
@ -35244,6 +35253,12 @@
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.24.2.tgz",
"integrity": "sha512-+TkAPBQw4Cp2bQrSPtPNkhET7XcWYjjDt1UjWYQs+xbT41q5OAl1I3IZyhg0drjn1nlC1K0f8sLVB/nshUcF1Q=="
},
"vue-load-image": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/vue-load-image/-/vue-load-image-0.1.12.tgz",
"integrity": "sha512-F2OwNP0hB0OG1wOy/r9SJ5oFiKvpgS1jluMkHtr3VFtbrhQPvuL53wNxatjkONn3Ciw6Ui54hBCgGBLAwwkZYw==",
"requires": {}
},
"vue-loader": {
"version": "15.9.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.3.tgz",

View File

@ -22,6 +22,7 @@
"vue-github-api": "^0.1.7",
"vue-headful": "^2.1.0",
"vue-i18n": "^8.22.4",
"vue-load-image": "^0.1.12",
"vue-observe-visibility": "^1.0.0",
"vue-plotly": "^1.1.0",
"vue-prism-editor": "^1.2.2",

View File

@ -0,0 +1,95 @@
<template>
<div id="historyAllPrintStatus" style="height: 250px; width: 100%;" v-observe-visibility="visibilityChanged"></div>
</template>
<script>
import { mapState } from 'vuex'
import * as echarts from 'echarts'
export default {
components: {
},
data: function() {
return {
chart : null,
chartOptions: {
darkMode: true,
animation: false,
grid: {
top: 10,
right: 0,
bottom: 0,
left: 10,
},
tooltip: {
trigger: 'item',
borderWidth: 0,
},
series: [{
type: 'pie',
data: [],
radius: '50%',
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
},
}
},
computed: {
...mapState({
}),
getAllPrintStatusArray: {
get: function() {
return this.$store.getters["server/history/getAllPrintStatusArray"]
}
},
},
methods: {
visibilityChanged (isVisible) {
if(isVisible && this.chart !== null) this.chart.resize()
},
createChart() {
if (document.getElementById("historyAllPrintStatus") && this.chart === null) {
this.chart = echarts.init(document.getElementById("historyAllPrintStatus"), null, { renderer: 'svg' })
this.chart.setOption(this.chartOptions)
this.updateChart()
} else
setTimeout(() => {
this.createChart()
}, 500)
},
updateChart() {
if (this.chart) {
const chartOptions = { series: this.chartOptions.series }
chartOptions.series[0].data = this.getAllPrintStatusArray
this.chart.setOption(chartOptions)
this.chart.resize()
}
},
},
created() {
window.addEventListener('resize', () => {
if (this.chart) this.chart.resize()
})
},
mounted: function() {
this.createChart()
},
watch: {
getAllPrintStatusArray: {
deep: true,
handler() {
this.updateChart()
}
},
}
}
</script>

View File

@ -0,0 +1,158 @@
<template>
<div id="historyFilamentUsage" style="height: 175px; width: 100%;" v-observe-visibility="visibilityChanged"></div>
</template>
<script>
import { mapState } from 'vuex'
import * as echarts from 'echarts'
export default {
components: {
},
data: function() {
return {
chart : null,
chartOptions: {
darkMode: true,
animation: false,
grid: {
top: 25,
right: 40,
bottom: 30,
left: 40,
},
tooltip: {
trigger: 'axis',
borderWidth: 0,
formatter: (datasets) => {
let output = ""
if (datasets.length) {
output = datasets[0]['marker']
let outputTime = datasets[0]['axisValueLabel']
outputTime = outputTime.substr(0, outputTime.indexOf(" ")+1)
let outputTimeDate = new Date(outputTime)
outputTime = outputTimeDate.toLocaleDateString()
let outputValue = Math.round(datasets[0]['data'][1] * 10) / 10
output += outputTime+": "+outputValue+"m"
}
return output
}
},
xAxis: {
type: 'time',
min: new Date().setHours(0,0,0) - 60*60*24*14*1000,
max: new Date().setHours(0,0,0),
minInterval: 60*60*24*1000,
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255, 255, 255, 0.06)',
},
},
axisLabel: {
color: 'rgba(255, 255, 255, 0.24)',
margin: 10,
},
},
yAxis: {
name: 'Filament [m]',
type: 'value',
minInterval: 10,
maxInterval: 100,
nameLocation: 'end',
nameGap: 5,
nameTextStyle: {
color: 'rgba(255, 255, 255, 0.24)',
align: 'left',
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.12)',
},
},
axisLabel: {
color: 'rgba(255, 255, 255, 0.24)',
formatter: '{value}',
//rotate: 90,
//showMaxLabel: false,
showMinLabel: true,
margin: 5,
},
axisLine: {
show: true,
lineStyle: {
color: 'rgba(255, 255, 255, 0.12)',
},
}
},
color: ['#BDBDBD'],
series: [{
type: 'bar',
data: [],
showSymbol: false
}],
},
}
},
computed: {
...mapState({
}),
filamentUsageArray: {
get: function() {
return this.$store.getters["server/history/getFilamentUsageArray"]
}
},
},
methods: {
visibilityChanged (isVisible) {
if(isVisible && this.chart !== null) this.chart.resize()
},
createChart() {
if (document.getElementById("historyFilamentUsage") && this.chart === null) {
this.chart = echarts.init(document.getElementById("historyFilamentUsage"), null, { renderer: 'canvas' })
this.chart.setOption(this.chartOptions)
this.updateChart()
} else setTimeout(() => {
this.createChart()
}, 500)
},
updateChart() {
if (this.chart) {
const chartOptions = { series: this.chartOptions.series }
chartOptions.series[0].data = this.filamentUsageArray
//chartOptions.color = [this.getPrimaryColor()]
this.chart.setOption(chartOptions)
this.chart.resize()
}
},
getPrimaryColor() {
if (this.$vuetify.theme.isDark) {
return this.$vuetify.theme.themes.dark.primary
} else {
return this.$vuetify.theme.themes.light.primary
}
}
},
created() {
window.addEventListener('resize', () => {
if (this.chart) this.chart.resize()
})
},
mounted: function() {
this.createChart()
},
watch: {
filamentUsageArray: {
deep: true,
handler() {
this.updateChart()
}
},
}
}
</script>

View File

@ -0,0 +1,140 @@
<template>
<div id="historyPrinttimeAvg" style="height: 175px; width: 100%;" v-observe-visibility="visibilityChanged"></div>
</template>
<script>
import { mapState } from 'vuex'
import * as echarts from 'echarts'
export default {
components: {
},
data: function() {
return {
chart : null,
chartOptions: {
darkMode: true,
animation: false,
grid: {
top: 25,
right: 40,
bottom: 30,
left: 40,
},
tooltip: {
trigger: 'item',
borderWidth: 0,
},
xAxis: {
type: 'category',
data: ['0-2h', '2-6h', '6-12h', '12-24h', '>24h'],
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255, 255, 255, 0.06)',
},
},
axisLabel: {
color: 'rgba(255, 255, 255, 0.24)',
margin: 10,
},
},
yAxis: {
name: 'Prints',
type: 'value',
minInterval: 10,
maxInterval: 100,
nameLocation: 'end',
nameGap: 5,
nameTextStyle: {
color: 'rgba(255, 255, 255, 0.24)',
align: 'left',
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.12)',
},
},
axisLabel: {
color: 'rgba(255, 255, 255, 0.24)',
formatter: '{value}',
//rotate: 90,
//showMaxLabel: false,
showMinLabel: true,
margin: 5,
},
axisLine: {
show: true,
lineStyle: {
color: 'rgba(255, 255, 255, 0.12)',
},
}
},
series: [{
type: 'bar',
data: [],
itemStyle: {
color: '#BDBDBD'
}
}]
},
}
},
computed: {
...mapState({
}),
printtimeAvgArray: {
get: function() {
return this.$store.getters["server/history/getPrinttimeAvgArray"]
}
},
},
methods: {
visibilityChanged (isVisible) {
if(isVisible && this.chart !== null) this.chart.resize()
},
createChart() {
if (document.getElementById("historyPrinttimeAvg") && this.chart === null) {
this.chart = echarts.init(document.getElementById("historyPrinttimeAvg"), null, { renderer: 'canvas' })
this.chart.setOption(this.chartOptions)
this.updateChart()
} else setTimeout(() => {
this.createChart()
}, 500)
},
updateChart() {
if (this.chart) {
const chartOptions = { series: this.chartOptions.series }
chartOptions.series[0].data = this.printtimeAvgArray
//chartOptions.series[0].itemStyle.color = this.getPrimaryColor()
this.chart.setOption(chartOptions)
}
},
getPrimaryColor() {
if (this.$vuetify.theme.isDark) {
return this.$vuetify.theme.themes.dark.primary
} else {
return this.$vuetify.theme.themes.light.primary
}
}
},
created() {
window.addEventListener('resize', () => {
if (this.chart) this.chart.resize()
})
},
mounted: function() {
this.createChart()
},
watch: {
filamentUsageArray: {
deep: true,
handler() {
this.updateChart()
}
},
}
}
</script>

View File

@ -0,0 +1,496 @@
<template>
<div>
<v-card>
<v-card-title>
{{ $t('History.PrintHistory') }}
<v-spacer class="d-none d-sm-block"></v-spacer>
<v-item-group class="v-btn-toggle my-5 my-sm-0 col-12 col-sm-auto px-0 py-0" name="controllers">
<v-btn title="Refresh History" class="flex-grow-1" @click="refreshHistory"><v-icon>mdi-refresh</v-icon></v-btn>
<v-menu :offset-y="true" :close-on-content-click="false" title="Setup current list">
<template v-slot:activator="{ on, attrs }">
<v-btn class="flex-grow-1" v-bind="attrs" v-on="on"><v-icon class="">mdi-cog</v-icon></v-btn>
</template>
<v-list>
<v-list-item class="minHeight36" v-for="header of configHeaders" v-bind:key="header.key">
<v-checkbox class="mt-0" hide-details v-model="header.visible" @change="changeColumnVisible(header.value)" :label="header.text"></v-checkbox>
</v-list-item>
</v-list>
</v-menu>
</v-item-group>
</v-card-title>
<v-card-text>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
:label="$t('History.Search')"
single-line
hide-details
></v-text-field>
</v-card-text>
<v-data-table
:items="jobs"
class="files-table"
:headers="filteredHeaders"
:options="options"
:custom-sort="sortFiles"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
:items-per-page.sync="countPerPage"
:footer-props="{
itemsPerPageText: $t('History.Jobs'),
itemsPerPageOptions: [10,25,50,100,-1]
}"
item-key="name"
:search="search"
:custom-filter="advancedSearch"
mobile-breakpoint="0">
<template slot="items" slot-scope="props">
<td v-for="header in filteredHeaders" v-bind:key="header.text">{{ props.item[header.value] }}</td>
</template>
<template #no-data>
<div class="text-center">{{ $t('History.Empty') }}</div>
</template>
<template #item="{ item }">
<tr
v-longpress:600="(e) => showContextMenu(e, item)"
@contextmenu="showContextMenu($event, item)"
@click="clickRow(item)"
:class="'file-list-cursor user-select-none '+(item.exists ? '' : 'text--disabled')"
>
<td class="pr-0 text-center" style="width: 32px;">
<template v-if="!item.exists">
<v-icon class="text--disabled">mdi-file-cancel</v-icon>
</template>
<template v-else-if="getSmallThumbnail(item) && getBigThumbnail(item)">
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<vue-load-image>
<img slot="image" :src="getSmallThumbnail(item)" width="32" height="32" v-bind="attrs" v-on="on" />
<v-progress-circular slot="preloader" indeterminate color="primary"></v-progress-circular>
<v-icon slot="error">mdi-file</v-icon>
</vue-load-image>
</template>
<span><img :src="getBigThumbnail(item)" width="250" /></span>
</v-tooltip>
</template>
<template v-else-if="getSmallThumbnail(item)">
<vue-load-image>
<img slot="image" :src="getSmallThumbnail(item)" width="32" height="32" />
<v-progress-circular slot="preloader" indeterminate color="primary"></v-progress-circular>
<v-icon slot="error">mdi-file</v-icon>
</vue-load-image>
</template>
<template v-else>
<v-icon>mdi-file</v-icon>
</template>
</td>
<td class=" ">{{ item.filename }}</td>
<td class="text-center">
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">
<v-icon small :color="getStatusColor(item.status)" :disabled="!item.exists">{{ getStatusIcon(item.status) }}</v-icon>
</span>
</template>
<span>{{ item.status.replaceAll("_", " ") }}</span>
</v-tooltip>
</td>
<td class=" " v-if="headers.find(header => header.value === 'size').visible">{{ 'size' in item.metadata && item.metadata.size ? formatFilesize(item.metadata.size) : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'modified').visible">{{ 'modified' in item.metadata && item.metadata.modified ? formatDate(item.metadata.modified) : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'start_time').visible">{{ item.start_time > 0 ? formatDate(item.start_time) : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'end_time').visible">{{ item.end_time > 0 ? formatDate(item.end_time) : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'estimated_time').visible">{{ 'estimated_time' in item.metadata && item.metadata.estimated_time ? formatPrintTime(item.metadata.estimated_time) : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'print_duration').visible">{{ item.print_duration > 0 ? formatPrintTime(item.print_duration) : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'total_duration').visible">{{ item.total_duration > 0 ? formatPrintTime(item.total_duration) : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'filament_total').visible">{{ 'filament_total' in item.metadata && item.metadata.filament_total ? item.metadata.filament_total+' mm' : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'filament_used').visible">{{ item.filament_used ? item.filament_used.toFixed()+' mm' : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'first_layer_extr_temp').visible">{{ 'first_layer_extr_temp' in item.metadata && item.metadata.first_layer_extr_temp ? item.metadata.first_layer_extr_temp+' °C' : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'first_layer_bed_temp').visible">{{ 'first_layer_bed_temp' in item.metadata && item.metadata.first_layer_bed_temp ? item.metadata.first_layer_bed_temp+' °C' : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'first_layer_height').visible">{{ 'first_layer_height' in item.metadata && item.metadata.first_layer_height ? item.metadata.first_layer_height+' mm' : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'layer_height').visible">{{ 'layer_height' in item.metadata && item.metadata.layer_height ? item.metadata.layer_height+' mm' : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'object_height').visible">{{ 'object_height' in item.metadata && item.metadata.object_height ? item.metadata.object_height+' mm' : '--' }}</td>
<td class=" " v-if="headers.find(header => header.value === 'slicer').visible">
{{ 'slicer' in item.metadata && item.metadata.slicer ? item.metadata.slicer : '--' }}
<small v-if="'slicer_version' in item.metadata && item.metadata.slicer_version"><br />{{ item.metadata.slicer_version }}</small>
</td>
</tr>
</template>
</v-data-table>
</v-card>
<v-menu v-model="contextMenu.shown" :position-x="contextMenu.x" :position-y="contextMenu.y" absolute offset-y>
<v-list>
<v-list-item @click="clickRow(contextMenu.item)">
<v-icon class="mr-1">mdi-text-box-search</v-icon> {{ $t('History.Details') }}
</v-list-item>
<v-list-item @click="startPrint(contextMenu.item)" v-if="contextMenu.item.exists" :disabled="is_printing">
<v-icon class="mr-1">mdi-printer</v-icon> {{ $t('History.Reprint') }}
</v-list-item>
<v-list-item @click="deleteJob(contextMenu.item)" :disabled="contextMenu.item.status === 'in_progress'">
<v-icon class="mr-1">mdi-delete</v-icon> {{ $t('History.Delete') }}
</v-list-item>
</v-list>
</v-menu>
<v-dialog v-model="detailsDialog.boolShow" :max-width="600" :max-height="500">
<v-card dark>
<v-toolbar flat dense >
<v-toolbar-title>
<span class="subheading"><v-icon left>mdi-update</v-icon>{{ $t('History.JobDetails') }}</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn small class="minwidth-0" color="grey darken-3" @click="detailsDialog.boolShow = false"><v-icon small>mdi-close-thick</v-icon></v-btn>
</v-toolbar>
<v-card-text class="pt-5">
<v-simple-table
style="
height: 350px;
max-height: 350px;
overflow-y: auto;
">
<tbody>
<tr>
<td>{{ $t('History.Filename') }}</td>
<td class="text-right">{{ detailsDialog.item.filename }}</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'size' in detailsDialog.item.metadata">
<td>{{ $t('History.Filesize') }}</td>
<td class="text-right">{{ formatFilesize(detailsDialog.item.metadata.size) }}</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'modified' in detailsDialog.item.metadata">
<td>{{ $t('History.LastModified') }}</td>
<td class="text-right">{{ formatDate(detailsDialog.item.metadata.modified) }}</td>
</tr>
<tr>
<td>{{ $t('History.Status') }}</td>
<td class="text-right">{{ detailsDialog.item.status }}</td>
</tr>
<tr>
<td>{{ $t('History.StartTime') }}</td>
<td class="text-right">{{ formatDate(detailsDialog.item.start_time) }}</td>
</tr>
<tr>
<td>{{ $t('History.EndTime') }}</td>
<td class="text-right">{{ formatDate(detailsDialog.item.end_time) }}</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'estimated_time' in detailsDialog.item.metadata">
<td>{{ $t('History.EstimatedTime') }}</td>
<td class="text-right">{{ formatPrintTime(detailsDialog.item.metadata.estimated_time) }}</td>
</tr>
<tr v-if="detailsDialog.item.print_duration > 0">
<td>{{ $t('History.PrintDuration') }}</td>
<td class="text-right">{{ formatPrintTime(detailsDialog.item.print_duration) }}</td>
</tr>
<tr v-if="detailsDialog.item.total_duration > 0">
<td>{{ $t('History.TotalDuration') }}</td>
<td class="text-right">{{ formatPrintTime(detailsDialog.item.total_duration) }}</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'filament_total' in detailsDialog.item.metadata">
<td>{{ $t('History.EstimatedFilament') }}</td>
<td class="text-right">{{ Math.round(detailsDialog.item.metadata.filament_total) }} mm</td>
</tr>
<tr v-if="detailsDialog.item.filament_used > 0">
<td>{{ $t('History.FilamentUsed') }}</td>
<td class="text-right">{{ Math.round(detailsDialog.item.filament_used) }} mm</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'first_layer_extr_temp' in detailsDialog.item.metadata">
<td>{{ $t('History.FirstLayerExtTemp') }}</td>
<td class="text-right">{{ detailsDialog.item.metadata.first_layer_extr_temp }} °C</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'first_layer_bed_temp' in detailsDialog.item.metadata">
<td>{{ $t('History.FirstLayerBedTemp') }}</td>
<td class="text-right">{{ detailsDialog.item.metadata.first_layer_bed_temp }} °C</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'first_layer_height' in detailsDialog.item.metadata">
<td>{{ $t('History.FirstLayerHeight') }}</td>
<td class="text-right">{{ detailsDialog.item.metadata.first_layer_height }} mm</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'layer_height' in detailsDialog.item.metadata">
<td>{{ $t('History.LayerHeight') }}</td>
<td class="text-right">{{ detailsDialog.item.metadata.layer_height }} mm</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'object_height' in detailsDialog.item.metadata">
<td>{{ $t('History.ObjectHeight') }}</td>
<td class="text-right">{{ detailsDialog.item.metadata.object_height }} mm</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'slicer' in detailsDialog.item.metadata">
<td>{{ $t('History.Slicer') }}</td>
<td class="text-right">{{ detailsDialog.item.metadata.slicer }}</td>
</tr>
<tr v-if="'metadata' in detailsDialog.item && 'slicer_version' in detailsDialog.item.metadata">
<td>{{ $t('History.SlicerVersion') }}</td>
<td class="text-right">{{ detailsDialog.item.metadata.slicer_version }}</td>
</tr>
</tbody>
</v-simple-table>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
import {mapGetters, mapState} from 'vuex'
import VueLoadImage from 'vue-load-image'
export default {
components: {
'vue-load-image': VueLoadImage
},
data() {
return {
search: '',
sortBy: 'start_time',
sortDesc: true,
selected: [],
hideHeaderColums: [],
headers: [
{ text: '', value: '', align: 'left', configable: false, visible: true, filterable: false },
{ text: this.$t("History.Filename"), value: 'filename', align: 'left', configable: false, visible: true },
{ text: '', value: 'status', align: 'left', configable: false, visible: true, filterable: false },
{ text: this.$t("History.Filesize"), value: 'size', align: 'left', configable: true, visible: false },
{ text: this.$t("History.LastModified"), value: 'modified', align: 'left', configable: true, visible: false },
{ text: this.$t("History.StartTime"), value: 'start_time', align: 'left', configable: true, visible: true },
{ text: this.$t("History.EndTime"), value: 'end_time', align: 'left', configable: true, visible: false },
{ text: this.$t("History.EstimatedTime"), value: 'estimated_time', align: 'left', configable: true, visible: true },
{ text: this.$t("History.PrintTime"), value: 'print_duration', align: 'left', configable: true, visible: true },
{ text: this.$t("History.TotalTime"), value: 'total_duration', align: 'left', configable: true, visible: false },
{ text: this.$t("History.FilamentCalc"), value: 'filament_total', align: 'left', configable: true, visible: false },
{ text: this.$t("History.FilamentUsed"), value: 'filament_used', align: 'left', configable: true, visible: true },
{ text: this.$t("History.FirstLayerExtTemp"), value: 'first_layer_extr_temp', align: 'left', configable: true, visible: false },
{ text: this.$t("History.FirstLayerBedTemp"), value: 'first_layer_bed_temp', align: 'left', configable: true, visible: false },
{ text: this.$t("History.FirstLayerHeight"), value: 'first_layer_height', align: 'left', configable: true, visible: false },
{ text: this.$t("History.LayerHeight"), value: 'layer_height', align: 'left', configable: true, visible: false },
{ text: this.$t("History.ObjectHeight"), value: 'object_height', align: 'left', configable: true, visible: false },
{ text: this.$t("History.Slicer"), value: 'slicer', align: 'left', configable: true, visible: true },
],
options: {
},
contextMenu: {
shown: false,
touchTimer: undefined,
x: 0,
y: 0,
item: {}
},
detailsDialog: {
item: {},
boolShow: false,
}
}
},
computed: {
...mapState({
jobs: state => state.server.history.jobs,
}),
...mapGetters( {
is_printing: "is_printing",
getStatusIcon: "server/history/getPrintStatusChipIcon",
getStatusColor: "server/history/getPrintStatusChipColor",
}),
configHeaders() {
return this.headers.filter(header => header.configable === true)
},
filteredHeaders() {
return this.headers.filter(header => header.visible === true)
},
baseUrl: {
get: function() {
return this.$store.getters["socket/getUrl"]
}
},
countPerPage: {
get: function() {
return this.$store.state.gui.history.countPerPage
},
set: function(newVal) {
return this.$store.dispatch("gui/setSettings", { history: { countPerPage: newVal } })
}
},
hideColums: {
get: function() {
return this.$store.state.gui.history.hideColums
},
set: function(newVal) {
return this.$store.dispatch("gui/setSettings", { history: { hideColums: newVal } })
}
},
},
mounted() {
this.hideColums.forEach((key) => {
let headerElement = this.headers.find(element => element.value === key)
if (headerElement) headerElement.visible = false
})
},
methods: {
refreshHistory: function() {
this.$socket.sendObj('server.history.list', {}, 'server/history/getHistory')
},
formatDate(date) {
let tmp2 = new Date(date*1000)
return tmp2.toLocaleString().replace(',', '')
},
formatFilesize(fileSizeInBytes) {
let i = -1
let byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']
do {
fileSizeInBytes = fileSizeInBytes / 1024
i++
} while (fileSizeInBytes > 1024)
return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i]
},
formatPrintTime(totalSeconds) {
if (totalSeconds) {
let output = ""
let days = Math.floor(totalSeconds / (3600 * 24))
if (days) {
totalSeconds %= (3600 * 24)
output += days+"d"
}
let hours = Math.floor(totalSeconds / 3600)
totalSeconds %= 3600
if (hours) output += " "+hours+"h"
let minutes = Math.floor(totalSeconds / 60)
if (minutes) output += " "+minutes+"m"
let seconds = totalSeconds % 60
if (seconds) output += " "+seconds.toFixed(0)+"s"
return output
}
return '--'
},
clickRow(item) {
this.detailsDialog.item = item
this.detailsDialog.boolShow = true
},
showContextMenu (e, item) {
if (!this.contextMenu.shown) {
e?.preventDefault();
this.contextMenu.shown = true
this.contextMenu.x = e?.clientX || e?.pageX || window.screenX / 2;
this.contextMenu.y = e?.clientY || e?.pageY || window.screenY / 2;
this.contextMenu.item = item
this.$nextTick(() => {
this.contextMenu.shown = true
})
}
},
sortFiles(items, sortBy, sortDesc) {
sortBy = sortBy.length ? sortBy[0] : 'filename';
sortDesc = sortDesc[0];
if (items !== undefined) {
// Sort by index
items.sort(function(a, b) {
if (a[sortBy] === b[sortBy]) {
return 0;
}
if (a[sortBy] === null || a[sortBy] === undefined) {
return -1;
}
if (b[sortBy] === null || b[sortBy] === undefined) {
return 1;
}
if (a[sortBy].constructor === String && b[sortBy].constructor === String) {
return a[sortBy].localeCompare(b[sortBy], undefined, { sensivity: 'base' });
}
if (a[sortBy] instanceof Array && b[sortBy] instanceof Array) {
const reducedA = a[sortBy].length ? a.filament.reduce((a, b) => a + b) : 0;
const reducedB = b[sortBy].length ? b.filament.reduce((a, b) => a + b) : 0;
return reducedA - reducedB;
}
return a[sortBy] - b[sortBy];
});
// Deal with descending order
if (sortDesc) items.reverse()
}
return items;
},
advancedSearch: function(value, search) {
return value != null &&
search != null &&
typeof value === 'string' &&
value.toString().toLowerCase().indexOf(search.toLowerCase()) !== -1
},
changeColumnVisible: function(name) {
if (this.headers.filter(header => header.value === name).length) {
let value = this.headers.filter(header => header.value === name)[0].visible;
this.$store.dispatch("gui/setHistoryColumns", {name: name, value: value});
}
},
startPrint(item) {
if (item.exists) this.$socket.sendObj('printer.print.start', { filename: item.filename }, 'switchToDashboard')
},
deleteJob(item) {
this.$socket.sendObj('server.history.delete_job', { uid: item.job_id }, 'server/history/getDeletedJobs')
},
getSmallThumbnail(item) {
if (
'metadata' in item &&
'thumbnails' in item.metadata &&
item.metadata.thumbnails.length
) {
const thumbnail = item.metadata.thumbnails.find(thumb =>
thumb.width >= 32 && thumb.width <= 64 &&
thumb.height >= 32 && thumb.height <= 64
)
let relative_url = ""
if (item.filename.lastIndexOf("/") !== -1) {
relative_url = item.filename.substr(0, item.filename.lastIndexOf("/")+1)
}
if (thumbnail && 'relative_path' in thumbnail) return this.baseUrl+"/server/files/gcodes/"+relative_url+thumbnail.relative_path
}
return false
},
getBigThumbnail(item) {
if (
'metadata' in item &&
'thumbnails' in item.metadata &&
item.metadata.thumbnails.length
) {
const thumbnail = item.metadata.thumbnails.find(thumb => thumb.width >= 300 && thumb.width <= 400)
let relative_url = ""
if (item.filename.lastIndexOf("/") !== -1) {
relative_url = item.filename.substr(0, item.filename.lastIndexOf("/")+1)
}
if (thumbnail && 'relative_path' in thumbnail) return this.baseUrl+"/server/files/gcodes/"+relative_url+thumbnail.relative_path
}
return false
},
getThumbnailWidth(item) {
if (this.getBigThumbnail(item)) {
const thumbnail = item.metadata.thumbnails.find(thumb => thumb.width >= 300 && thumb.width <= 400)
if (thumbnail) return thumbnail.width
}
return 400
},
},
watch: {
hideColums: function(newVal) {
newVal.forEach((key) => {
let headerElement = this.headers.find(element => element.value === key)
if (headerElement) headerElement.visible = false
})
}
}
}
</script>

View File

@ -300,22 +300,22 @@
},
estimated_time_file: {
get() {
return this.$store.getters.getEstimatedTimeFile
return this.$store.getters["printer/getEstimatedTimeFile"]
}
},
estimated_time_filament: {
get() {
return this.$store.getters.getEstimatedTimeFilament
return this.$store.getters["printer/getEstimatedTimeFilament"]
}
},
estimated_time_slicer: {
get() {
return this.$store.getters.getEstimatedTimeSlicer
return this.$store.getters["printer/getEstimatedTimeSlicer"]
}
},
eta: {
get() {
return this.$store.getters.getEstimatedTimeETA
return this.$store.getters["printer/getEstimatedTimeETA"]
}
}
},

View File

@ -12,6 +12,7 @@ import WebcamPanel from "./WebcamPanel";
import MiniconsolePanel from "./MiniconsolePanel";
import Settings from "./Settings/";
import PowerControlPanel from "./PowerControlPanel.vue";
import HistoryListPanel from "./HistoryListPanel.vue";
Vue.component('status-panel', StatusPanel);
Vue.component('klippy-state-panel', KlippyStatePanel);
@ -24,6 +25,7 @@ Vue.component('miscellaneous-panel', Miscellaneous);
Vue.component('webcam-panel', WebcamPanel);
Vue.component('miniconsole-panel', MiniconsolePanel);
Vue.component('power-control-panel', PowerControlPanel);
Vue.component('history-list-panel', HistoryListPanel);
export default {
Settings

View File

@ -46,6 +46,7 @@ export default {
Console: "Console",
Heightmap: "Heightmap",
"G-Code Files": "G-Code Files",
History: "History",
Machine: "Machine",
Interface: "Interface"
},
@ -121,6 +122,45 @@ export default {
DoYouReallyWantToDelete: "Do you really want to delete the profile",
Remove: "remove"
},
History: {
Statistics: "Statistics",
TotalPrinttime: "Total Printtime",
LongestPrinttime: "Longest Printtime",
AvgPrinttime: "Avg. Printtime",
TotalFilamentUsed: "Total Filament Used",
TotalJobs: "Total Jobs",
FilamentUsage: "Filament usage",
PrinttimeAvg: "Printtime AVG",
PrintHistory: "Print History",
Search: "search",
Jobs: "Jobs",
Empty: "empty",
Details: "Details",
Reprint: "Reprint",
Delete: "Delete",
JobDetails: "Job Details",
Filename: "Filename",
Filesize: "Filesize",
LastModified: "Last Modified",
Status: "Status",
StartTime: "Start Time",
EndTime: "End Time",
EstimatedTime: "Estimated Time",
PrintDuration: "Print Time",
PrintTime: "Print Time",
TotalDuration: "Total Time",
TotalTime: "Total Time",
EstimatedFilament: "Estimated Filament",
FilamentCalc: "Filament Calc",
FilamentUsed: "Filament Used",
FirstLayerExtTemp: "First Layer Ext. Temp.",
FirstLayerBedTemp: "First Layer Bed Temp.",
FirstLayerHeight: "First Layer Height",
LayerHeight: "Layer Height",
ObjectHeight: "Object Height",
Slicer: "Slicer",
SlicerVersion: "Slicer Version",
},
Panels: {
ControlPanel: {
Controls: "Controls",

View File

@ -66,6 +66,9 @@
<v-list-item class="minHeight36">
<v-checkbox class="mt-0" hide-details v-model="showHiddenFiles" :label="$t('Files.HiddenFiles')"></v-checkbox>
</v-list-item>
<v-list-item class="minHeight36">
<v-checkbox class="mt-0" hide-details v-model="showPrintedFiles" label="Printed files"></v-checkbox>
</v-list-item>
<v-divider></v-divider>
<v-list-item class="minHeight36" v-for="header of configHeaders" v-bind:key="header.key">
<v-checkbox class="mt-0" hide-details v-model="header.visible" @change="changeMetadataVisible(header.value)" :label="header.text"></v-checkbox>
@ -147,25 +150,53 @@
@dragover="dragOverFilelist($event, item)" @dragleave="dragLeaveFilelist" @drop.prevent.stop="dragDropFilelist($event, item)"
:data-name="item.filename"
>
<td class="pr-0 text-center" style="width: 32px;">
<v-icon v-if="item.isDirectory">mdi-folder</v-icon>
<v-icon v-if="!item.isDirectory && !(getSmallThumbnail(item))">mdi-file</v-icon>
<v-tooltip v-if="!item.isDirectory && getSmallThumbnail(item) && getBigThumbnail(item)" top>
<template v-slot:activator="{ on, attrs }">
<img :src="getSmallThumbnail(item)" width="32" height="32" v-bind="attrs" v-on="on" />
<td :class="'pr-0 text-center jobStatus '+getJobStatus(item)" style="width: 32px;">
<template v-if="item.isDirectory">
<v-icon>mdi-folder</v-icon>
</template>
<template v-else>
<template v-if="getSmallThumbnail(item) && getBigThumbnail(item)">
<v-tooltip v-if="!item.isDirectory && getSmallThumbnail(item) && getBigThumbnail(item)" top>
<template v-slot:activator="{ on, attrs }">
<vue-load-image>
<img slot="image" :src="getSmallThumbnail(item)" width="32" height="32" v-bind="attrs" v-on="on" />
<v-progress-circular slot="preloader" indeterminate color="primary"></v-progress-circular>
<v-icon slot="error">mdi-file</v-icon>
</vue-load-image>
</template>
<span><img :src="getBigThumbnail(item)" width="250" /></span>
</v-tooltip>
</template>
<span><img :src="getBigThumbnail(item)" width="250" /></span>
</v-tooltip>
<img v-if="!item.isDirectory && getSmallThumbnail(item) && !getBigThumbnail(item)" :src="getSmallThumbnail(item)" width="32" height="32" />
<template v-else-if="getSmallThumbnail(item)">
<vue-load-image>
<img slot="image" :src="getSmallThumbnail(item)" width="32" height="32" />
<v-progress-circular slot="preloader" indeterminate color="primary"></v-progress-circular>
<v-icon slot="error">mdi-file</v-icon>
</vue-load-image>
</template>
<template v-else>
<v-icon>mdi-file</v-icon>
</template>
</template>
</td>
<td class=" ">{{ item.filename }}</td>
<td class="text-no-wrap text-right" v-if="headers.filter(header => header.value === 'size')[0].visible">{{ item.isDirectory ? '--' : formatFilesize(item.size) }}</td>
<td class="text-right" v-if="headers.filter(header => header.value === 'modified')[0].visible">{{ formatDate(item.modified) }}</td>
<td class="text-no-wrap text-right" v-if="headers.filter(header => header.value === 'object_height')[0].visible">{{ item.object_height ? item.object_height.toFixed(2)+' mm' : '--' }}</td>
<td class="text-no-wrap text-right" v-if="headers.filter(header => header.value === 'layer_height')[0].visible">{{ item.layer_height ? item.layer_height.toFixed(2)+' mm' : '--' }}</td>
<td class="text-no-wrap text-right" v-if="headers.filter(header => header.value === 'filament_total')[0].visible">{{ item.filament_total ? item.filament_total.toFixed()+' mm' : '--' }}</td>
<td class="text-no-wrap text-right" v-if="headers.filter(header => header.value === 'estimated_time')[0].visible">{{ formatPrintTime(item.estimated_time) }}</td>
<td class="text-no-wrap text-right" v-if="headers.filter(header => header.value === 'slicer')[0].visible">{{ item.slicer ? item.slicer : '--' }}<br /><small v-if="item.slicer_version">{{ item.slicer_version}}</small></td>
<td class="text-center">
<v-tooltip top v-if="getJobStatus(item)">
<template v-slot:activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">
<v-icon small :color="getStatusColor(getJobStatus(item))">{{ getStatusIcon(getJobStatus(item)) }}</v-icon>
</span>
</template>
<span>{{ getJobStatus(item).replaceAll("_", " ") }}</span>
</v-tooltip>
</td>
<td class="text-no-wrap text-right" v-if="headers.find(header => header.value === 'size').visible">{{ item.isDirectory ? '--' : formatFilesize(item.size) }}</td>
<td class="text-right" v-if="headers.find(header => header.value === 'modified').visible">{{ formatDate(item.modified) }}</td>
<td class="text-no-wrap text-right" v-if="headers.find(header => header.value === 'object_height').visible">{{ item.object_height ? item.object_height.toFixed(2)+' mm' : '--' }}</td>
<td class="text-no-wrap text-right" v-if="headers.find(header => header.value === 'layer_height').visible">{{ item.layer_height ? item.layer_height.toFixed(2)+' mm' : '--' }}</td>
<td class="text-no-wrap text-right" v-if="headers.find(header => header.value === 'filament_total').visible">{{ item.filament_total ? item.filament_total.toFixed()+' mm' : '--' }}</td>
<td class="text-no-wrap text-right" v-if="headers.find(header => header.value === 'estimated_time').visible">{{ formatPrintTime(item.estimated_time) }}</td>
<td class="text-no-wrap text-right" v-if="headers.find(header => header.value === 'slicer').visible">{{ item.slicer ? item.slicer : '--' }}<br /><small v-if="item.slicer_version">{{ item.slicer_version}}</small></td>
</tr>
</template>
<v-data-footer>{{ $t('Files.blabla')}}</v-data-footer>
@ -295,8 +326,12 @@
import axios from 'axios'
import { findDirectory } from "@/plugins/helpers"
import { validGcodeExtensions } from "@/store/variables"
import VueLoadImage from "vue-load-image"
export default {
components: {
'vue-load-image': VueLoadImage
},
data () {
return {
search: '',
@ -332,6 +367,16 @@
{ text: this.$t('Files.FilamentUsage'), value: 'filament_total', align: 'right', configable: true, visible: true },
{ text: this.$t('Files.PrintTime'), value: 'estimated_time', align: 'right', configable: true, visible: true },
{ text: this.$t('Files.Slicer'), value: 'slicer', align: 'right', configable: true, visible: true },
{ text: '', value: '', align: 'left', configable: false, visible: true, filterable: false },
{ text: 'Name', value: 'filename', align: 'left', configable: false, visible: true },
{ text: '', value: 'status', align: 'left', configable: false, visible: true },
{ text: 'Filesize', value: 'size', align: 'right', configable: true, visible: true },
{ text: 'Last modified', value: 'modified', align: 'right', configable: true, visible: true },
{ text: 'Object Height', value: 'object_height', align: 'right', configable: true, visible: true },
{ text: 'Layer Height', value: 'layer_height', align: 'right', configable: true, visible: true },
{ text: 'Filament Usage', value: 'filament_total', align: 'right', configable: true, visible: true },
{ text: 'Print Time', value: 'estimated_time', align: 'right', configable: true, visible: true },
{ text: 'Slicer', value: 'slicer', align: 'right', configable: true, visible: true },
],
options: {
@ -384,9 +429,11 @@
displayMetadata: state => state.gui.gcodefiles.showMetadata,
}),
...mapGetters([
'is_printing'
]),
...mapGetters( {
is_printing: "is_printing",
getStatusIcon: "server/history/getPrintStatusChipIcon",
getStatusColor: "server/history/getPrintStatusChipColor",
}),
configHeaders() {
return this.headers.filter(header => header.configable === true)
},
@ -401,6 +448,14 @@
return this.$store.dispatch("gui/setSettings", { gcodefiles: { showHiddenFiles: newVal } })
}
},
showPrintedFiles: {
get: function() {
return this.$store.state.gui.gcodefiles.showPrintedFiles
},
set: function(newVal) {
return this.$store.dispatch("gui/setSettings", { gcodefiles: { showPrintedFiles: newVal } })
}
},
countPerPage: {
get: function() {
return this.$store.state.gui.gcodefiles.countPerPage
@ -657,6 +712,12 @@
if (!this.showHiddenFiles) {
this.files = this.files.filter(file => file.filename !== "thumbs" && file.filename.substr(0, 1) !== ".");
}
if (!this.showPrintedFiles) {
this.files = this.files.filter(file => this.$store.getters["server/history/getPrintStatus"]({
filename: (this.currentPath+"/"+file.filename).substr(7),
modified: new Date(file.modified).getTime()
}) !== 'completed')
}
},
startPrint(filename = "") {
filename = (this.currentPath+"/"+filename).substring(7)
@ -831,7 +892,13 @@
}
return 400
}
},
getJobStatus(item) {
return this.$store.getters["server/history/getPrintStatus"]({
filename: (this.currentPath+"/"+item.filename).substr(7),
modified: new Date(item.modified).getTime()
})
},
},
watch: {
filetree: {
@ -843,6 +910,13 @@
if (!this.showHiddenFiles) {
this.files = this.files.filter(file => file.filename !== "thumbs" && file.filename.substr(0, 1) !== ".");
}
if (!this.showPrintedFiles) {
this.files = this.files.filter(file => this.$store.getters["server/history/getPrintStatus"]({
filename: (this.currentPath+"/"+file.filename).substr(7),
modified: new Date(file.modified).getTime()
}) !== 'completed')
}
}
},
currentPath: {
@ -853,6 +927,13 @@
if (!this.showHiddenFiles) {
this.files = this.files.filter(file => file.filename !== "thumbs" && file.filename.substr(0, 1) !== ".");
}
if (!this.showPrintedFiles) {
this.files = this.files.filter(file => this.$store.getters["server/history/getPrintStatus"]({
filename: (this.currentPath+"/"+file.filename).substr(7),
modified: new Date(file.modified).getTime()
}) !== 'completed')
}
}
},
displayMetadata: {
@ -870,6 +951,9 @@
showHiddenFiles: function() {
this.loadPath();
},
showPrintedFiles: function() {
this.loadPath();
},
hideMetadataColums: function(newVal) {
newVal.forEach((key) => {
let headerElement = this.headers.find(element => element.value === key)

158
src/pages/History.vue Normal file
View File

@ -0,0 +1,158 @@
<style>
</style>
<template>
<div>
<v-row>
<v-col>
<v-card>
<v-toolbar flat dense >
<v-toolbar-title>
<span class="subheading"><v-icon left>mdi-chart-areaspline</v-icon>{{ $t('History.Statistics') }}</span>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="pa-0">
<v-row align="center">
<v-col class="col-12 col-sm-6 col-md-4">
<v-simple-table>
<tbody>
<tr>
<td>{{ $t('History.TotalPrinttime') }}</td>
<td class="text-right">{{ formatPrintTime(totalPrintTime) }}</td>
</tr>
<tr>
<td>{{ $t('History.LongestPrinttime') }}</td>
<td class="text-right">{{ formatPrintTime(longestPrintTime) }}</td>
</tr>
<tr>
<td>{{ $t('History.AvgPrinttime') }}</td>
<td class="text-right">{{ formatPrintTime(avgPrintTime) }}</td>
</tr>
<tr>
<td>{{ $t('History.TotalFilamentUsed') }}</td>
<td class="text-right">{{ Math.round(totalFilamentUsed / 100) / 10 }} m</td>
</tr>
<tr>
<td>{{ $t('History.TotalJobs') }}</td>
<td class="text-right">{{ totalJobsCount }}</td>
</tr>
</tbody>
</v-simple-table>
</v-col>
<v-col class="col-12 col-sm-6 col-md-4">
<history-all-print-status></history-all-print-status>
</v-col>
<v-col class="col-12 col-sm-12 col-md-4">
<history-filament-usage v-if="toggleChart === 'filament_usage'"></history-filament-usage>
<history-printtime-avg v-if="toggleChart === 'printtime_avg'"></history-printtime-avg>
<div class="text-center mt-3">
<v-btn-toggle v-model="toggleChart" small mandatory>
<v-btn small value="filament_usage">
{{ $t('History.FilamentUsage') }}
</v-btn>
<v-btn small value="printtime_avg">
{{ $t('History.PrinttimeAvg') }}
</v-btn>
</v-btn-toggle>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row class="mt-6">
<v-col>
<history-list-panel></history-list-panel>
</v-col>
</v-row>
</div>
</template>
<script>
import HistoryAllPrintStatus from "@/components/charts/HistoryAllPrintStatus"
import HistoryFilamentUsage from "@/components/charts/HistoryFilamentUsage"
import HistoryPrinttimeAvg from "@/components/charts/HistoryPrinttimeAvg"
import HistoryListPanel from "@/components/panels/HistoryListPanel"
export default {
components: {
HistoryAllPrintStatus,
HistoryFilamentUsage,
HistoryPrinttimeAvg,
HistoryListPanel
},
data () {
return {
}
},
computed: {
totalPrintTime: {
get() {
return 'total_print_time' in this.$store.state.server.history.job_totals ? this.$store.state.server.history.job_totals.total_print_time : 0
}
},
longestPrintTime: {
get() {
return 'longest_print' in this.$store.state.server.history.job_totals ? this.$store.state.server.history.job_totals.longest_print : 0
}
},
avgPrintTime: {
get() {
if (this.totalJobsCount > 0 && this.totalPrintTime > 0) return Math.round(this.totalPrintTime / this.totalJobsCount)
return 0
}
},
totalFilamentUsed: {
get() {
return 'total_filament_used' in this.$store.state.server.history.job_totals ? this.$store.state.server.history.job_totals.total_filament_used : 0
}
},
totalJobsCount: {
get() {
return 'total_jobs' in this.$store.state.server.history.job_totals ? this.$store.state.server.history.job_totals.total_jobs : 0
}
},
toggleChart: {
get() {
return this.$store.state.gui.history.toggleChartCol3
},
set: function(newVal) {
return this.$store.dispatch("gui/setSettings", { history: { toggleChartCol3: newVal } })
}
}
},
methods: {
formatPrintTime(totalSeconds) {
if (totalSeconds) {
let output = ""
let days = Math.floor(totalSeconds / (3600 * 24))
if (days) {
totalSeconds %= (3600 * 24)
output += days+"d"
}
let hours = Math.floor(totalSeconds / 3600)
totalSeconds %= 3600
if (hours) output += " "+hours+"h"
let minutes = Math.floor(totalSeconds / 60)
if (minutes) output += " "+minutes+"m"
let seconds = totalSeconds % 60
if (seconds) output += " "+seconds.toFixed(0)+"s"
return output
}
return '--'
},
},
watch: {
}
}
</script>

View File

@ -11,22 +11,25 @@ Vue.use(VueToast, {
})
Vue.use(Vuetify,{
components: {
VSnackbar,
VBtn,
VIcon,
}
options: {
customProperties: true
},
components: {
VSnackbar,
VBtn,
VIcon,
}
})
export default new Vuetify({
theme: {
themes: {
dark: {
theme: {
themes: {
dark: {
}
}
},
icons: {
iconfont: 'mdi',
},
}
}
},
icons: {
iconfont: 'mdi',
},
})

View File

@ -4,6 +4,7 @@ import Farm from '../pages/Farm.vue'
import Console from '../pages/Console.vue'
import Heightmap from '../pages/Heightmap.vue'
import Files from '../pages/Files.vue'
import History from '../pages/History.vue'
import Settings from '../pages/Settings.vue'
import SettingsInterface from '../pages/settings/interface.vue'
import SettingsMachine from '../pages/settings/machine.vue'
@ -56,6 +57,13 @@ const routes = [
component: Files,
alwaysShow: false,
},
{
title: "History",
path: '/history',
icon: 'history',
component: History,
alwaysShow: false,
},
{
title: "Settings",
path: '/settings',

View File

@ -5,14 +5,13 @@ export default {
getSidebarLogo: (state, getters, rootState, rootGetters) => {
let configDir = findDirectory(state.filetree, ['config', themeDir])
const acceptName = "sidebar-logo"
const acceptExtensions = ['svg', 'jpg', 'jpeg', 'png', 'gif']
let file = configDir.find(element =>
element.filename !== undefined && (
element.filename === 'sidebar-logo.svg' ||
element.filename === 'sidebar-logo.jpg' ||
element.filename === 'sidebar-logo.jpeg' ||
element.filename === 'sidebar-logo.png' ||
element.filename === 'sidebar-logo.gif'
element.filename.substr(0, element.filename.lastIndexOf('.')) === acceptName &&
acceptExtensions.includes(element.filename.substr(element.filename.lastIndexOf('.')+1))
)
)
if (file) return rootGetters["socket/getUrl"]+'/server/files/config/'+themeDir+'/'+file.filename
@ -22,13 +21,13 @@ export default {
getSidebarBackground: (state, getters, rootState, rootGetters) => {
let configDir = findDirectory(state.filetree, ['config', themeDir])
const acceptName = "sidebar-background"
const acceptExtensions = ['jpg', 'jpeg', 'png', 'gif']
let file = configDir.find(element =>
element.filename !== undefined && (
element.filename === 'sidebar-background.jpg' ||
element.filename === 'sidebar-background.jpeg' ||
element.filename === 'sidebar-background.png' ||
element.filename === 'sidebar-background.gif'
element.filename.substr(0, element.filename.lastIndexOf('.')) === acceptName &&
acceptExtensions.includes(element.filename.substr(element.filename.lastIndexOf('.')+1))
)
)
if (file) return rootGetters["socket/getUrl"]+'/server/files/config/'+themeDir+'/'+file.filename
@ -38,13 +37,13 @@ export default {
getMainBackground: (state, getters, rootState, rootGetters) => {
let configDir = findDirectory(state.filetree, ['config', themeDir])
const acceptName = "main-background"
const acceptExtensions = ['jpg', 'jpeg', 'png', 'gif']
let file = configDir.find(element =>
element.filename !== undefined && (
element.filename === 'main-background.jpg' ||
element.filename === 'main-background.jpeg' ||
element.filename === 'main-background.png' ||
element.filename === 'main-background.gif'
element.filename.substr(0, element.filename.lastIndexOf('.')) === acceptName &&
acceptExtensions.includes(element.filename.substr(element.filename.lastIndexOf('.')+1))
)
)
if (file) return 'url('+rootGetters["socket/getUrl"]+'/server/files/config/'+themeDir+'/'+file.filename+')'

View File

@ -22,6 +22,7 @@ export default {
item.isDirectory &&
'filename' in item &&
'dirs' in payload &&
payload.dirs !== undefined &&
payload.dirs.length > 0 &&
payload.dirs.findIndex(element => element.dirname === item.filename) < 0
) parent.splice(key, 1)

View File

@ -149,5 +149,13 @@ export default {
dispatch('farm/readStoredPrinters', {}, { root: true })
}
dispatch('printer/init', null, { root: true })
}
},
setHistoryColumns({ commit, dispatch, state }, data) {
commit('setHistoryColumns', data)
dispatch('updateSettings', {
keyName: 'history',
newVal: state.history
})
},
}

View File

@ -61,8 +61,14 @@ export function getDefaultState() {
gcodefiles: {
countPerPage: 10,
showHiddenFiles: false,
showPrintedFiles: true,
hideMetadataColums: []
},
history: {
countPerPage: 10,
toggleChartCol3: 'filament_usage',
hideColums: []
},
settings: {
configfiles: {
countPerPage: 10,

View File

@ -88,6 +88,13 @@ export default {
Vue.set(state.tempchart.datasetSettings[payload.name]['additionalSensors'], payload.sensor, {})
Vue.set(state.tempchart.datasetSettings[payload.name]['additionalSensors'][payload.sensor], 'boolList', payload.value)
},
}
setHistoryColumns(state, data) {
if (data.value && state.history.hideColums.includes(data.name)) {
state.history.hideColums.splice(state.history.hideColums.indexOf(data.name), 1)
} else if (!data.value && !state.history.hideColums.includes(data.name)) {
state.history.hideColums.push(data.name)
}
},
}

View File

@ -330,7 +330,7 @@ export default {
return additionValues
},
getTempListAdditionSensors: (state, getters, rootState, rootGetters) => (name) => {
let additionValues = {}
additionalSensors.forEach(sensorName => {
@ -578,7 +578,7 @@ export default {
return 0
},
getEstimatedTimeETA: (getters) => {
getEstimatedTimeETA: (state, getters) => {
let time = 0
let timeCount = 0

View File

@ -57,6 +57,11 @@ export default {
if (components.includes("update_manager") !== false)
Vue.prototype.$socket.sendObj('machine.update.status', {}, 'server/updateManager/getStatus')
if (payload.plugins.includes("history") !== false) {
Vue.prototype.$socket.sendObj('server.history.list', {}, 'server/history/getHistory')
Vue.prototype.$socket.sendObj('server.history.totals', {}, 'server/history/getTotals')
}
}
if (state.registered_directories.length === 0 && 'registered_directories' in payload) {

View File

@ -0,0 +1,35 @@
import Vue from "vue";
export default {
reset({ commit }) {
commit('reset')
},
getTotals({ commit }, payload) {
commit('setTotals', payload.job_totals)
},
getHistory({ commit }, payload) {
commit('reset')
payload.jobs.forEach(job => {
commit('addJob', job)
})
},
getChanged({ commit }, payload) {
if (payload.action === 'added') commit('addJob', payload.job)
else if (payload.action === 'finished') commit('updateJob', payload.job)
Vue.prototype.$socket.sendObj('server.history.totals', {}, 'server/history/getTotals')
},
getDeletedJobs({ commit }, payload) {
if ('deleted_jobs' in payload && Array.isArray(payload.deleted_jobs)) {
payload.deleted_jobs.forEach(jobId => {
commit('destroyJob', jobId)
})
}
}
}

View File

@ -0,0 +1,190 @@
export default {
getTotalPrintTime(state) {
let output = 0
state.jobs.forEach(current => {
output += current.print_duration
})
return output
},
getTotalCompletedPrintTime(state) {
let output = 0
state.jobs.forEach(current => {
if (current.status === "completed") output += current.print_duration
})
return output
},
getLongestPrintTime(state) {
let output = 0
state.jobs.forEach(current => {
if (current.print_duration > output) output = current.print_duration
})
return output
},
getTotalFilamentUsed(state) {
let output = 0
state.jobs.forEach(current => {
output += current.filament_used
})
return output
},
getTotalJobsCount(state) {
return state.jobs.length
},
getTotalCompletedJobsCount(state) {
return state.jobs.filter(job => job.status === "completed").length
},
getAvgPrintTime(state, getters) {
const totalCompletedPrintTime = getters.getTotalCompletedPrintTime
const totalCompletedJobsCount = getters.getTotalCompletedJobsCount
return totalCompletedPrintTime > 0 && totalCompletedJobsCount > 0 ? Math.round(totalCompletedPrintTime / totalCompletedJobsCount) : 0
},
getAllPrintStatusArray(state) {
let output = []
state.jobs.forEach(current => {
const index = output.findIndex(element => element.name === current.status)
if (index !== -1) output[index].value +=1
else {
let itemStyle = {
opacity: 0.9,
color: '#424242'
}
switch (current.status) {
case 'completed':
itemStyle['color'] = '#BDBDBD'
break
case 'in_progress':
itemStyle['color'] = '#EEEEEE'
break
case 'cancelled':
itemStyle['color'] = '#616161'
break
}
output.push({
name: current.status,
value: 1,
itemStyle: itemStyle,
label: {
color: '#fff'
}
})
}
})
return output
},
getFilamentUsageArray(state) {
let output = []
const startDate = new Date()
startDate.setDate(startDate.getDate() - 14)
startDate.setHours(0,0,0,0)
const jobsFiltered = state.jobs.filter(job => job.start_time * 1000 >= startDate && job.filament_used > 0)
for (let i = 0; i <= 14; i++) {
const tmpDate = new Date()
tmpDate.setDate(startDate.getDate() + i)
output.push([
new Date(tmpDate).setHours(0,0,0,0),
0
])
}
if (jobsFiltered.length) {
jobsFiltered.forEach(current => {
const currentStartDate = new Date(current.start_time * 1000).setHours(0,0,0,0)
const index = output.findIndex(element => element[0] === currentStartDate)
if (index !== -1) output[index][1] += Math.round(current.filament_used) / 1000
})
}
return output.sort((a,b) => {
return b[0] - a[0]
})
},
getPrinttimeAvgArray(state) {
let output = [0,0,0,0,0]
const startDate = new Date(new Date().getDate() - 14)
const jobsFiltered = state.jobs.filter(job => job.start_time * 1000 >= startDate && job.status === 'completed')
if (jobsFiltered.length) {
jobsFiltered.forEach(current => {
if (current.print_duration > 0 && current.print_duration <= 60*60*2) output[0]++
else if (current.print_duration > 60*60*2 && current.print_duration <= 60*60*6) output[1]++
else if (current.print_duration > 60*60*6 && current.print_duration <= 60*60*12) output[2]++
else if (current.print_duration > 60*60*12 && current.print_duration <= 60*60*24) output[3]++
else if (current.print_duration > 60*60*24) output[4]++
})
}
return output
},
getPrintStatus: (state) => (file) => {
if (state.jobs.length) {
const jobs = state.jobs.filter(job =>
'filename' in job &&
job.filename === file.filename &&
'status' in job &&
'metadata' in job &&
'modified' in job.metadata &&
parseInt(job.metadata.modified * 1000) === file.modified
)
if (jobs.length > 1) {
jobs.sort((a,b) => {
return b.start_time - a.start_time
})
return jobs[0].status
} else if (jobs.length === 1) {
return jobs[0].status
}
}
return ""
},
getPrintStatusChipColor: () => (status) => {
switch(status) {
case 'in_progress': return 'blue accent-3' //'blue-grey darken-1'
case 'completed': return 'green' //'green'
case 'cancelled': return 'red'
default: return 'orange'
}
},
getPrintStatusChipIcon: () => (status) => {
switch(status) {
case 'in_progress': return 'mdi-progress-clock'
case 'completed': return 'mdi-checkbox-marked-circle-outline'
case 'cancelled': return 'mdi-close-circle-outline'
default: return 'mdi-alert-outline'
}
}
}

View File

@ -0,0 +1,21 @@
import actions from './actions'
import mutations from './mutations'
import getters from './getters'
export function getDefaultState() {
return {
jobs: [],
job_totals: {}
}
}
// initial state
const state = getDefaultState()
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -0,0 +1,30 @@
import { getDefaultState } from './index'
import Vue from "vue";
export default {
reset(state) {
Object.assign(state, getDefaultState())
},
setTotals(state, payload) {
Vue.set(state, 'job_totals', payload)
},
addJob(state, payload) {
state.jobs.push(payload)
},
updateJob(state, payload) {
const index = state.jobs.findIndex(job => job.job_id === payload.job_id)
if (index !== -1) {
Vue.set(state.jobs, index, payload)
}
},
destroyJob(state, payload) {
const index = state.jobs.findIndex(job => job.job_id === payload)
if (index !== -1) {
state.jobs.splice(index,1)
}
}
}

View File

@ -5,6 +5,7 @@ import getters from './getters'
// import modules
import power from './power'
import updateManager from './updateManager'
import history from './history'
// create getDefaultState
export function getDefaultState() {
@ -32,5 +33,6 @@ export default {
modules: {
power,
updateManager,
history,
}
}

View File

@ -31,7 +31,7 @@ export default {
if (event.wasClean) window.console.log('Socket closed clear')
},
onMessage ({ commit, state }, payload) {
onMessage ({ commit, state, dispatch }, payload) {
if (!state.isConnected) commit('setConnected')
switch(payload.method) {
@ -99,6 +99,10 @@ export default {
commit('server/updateManager/setStatus', payload.params[0], { root: true })
break
case 'notify_history_changed':
dispatch('server/history/getChanged', payload.params[0], { root: true })
break
default:
if (payload.result !== "ok") {
if (