feat: Reminders panel on the History page (#1274)
Co-authored-by: Stefan Dej <meteyou@gmail.com>
This commit is contained in:
parent
c7810748c6
commit
866a0c6a49
@ -60,7 +60,7 @@ export default class HistoryAllPrintStatusChart extends Mixins(BaseMixin, ThemeM
|
||||
}
|
||||
|
||||
get selectedJobs() {
|
||||
return this.$store.state.gui.view.history.selectedJobs ?? []
|
||||
return this.$store.getters['server/history/getSelectedJobs']
|
||||
}
|
||||
|
||||
get allPrintStatusArray() {
|
||||
|
@ -20,7 +20,7 @@ import { ServerHistoryStateAllPrintStatusEntry } from '@/store/server/history/ty
|
||||
})
|
||||
export default class HistoryAllPrintStatusTable extends Mixins(BaseMixin) {
|
||||
get selectedJobs() {
|
||||
return this.$store.state.gui.view.history.selectedJobs ?? []
|
||||
return this.$store.getters['server/history/getSelectedJobs']
|
||||
}
|
||||
|
||||
get allPrintStatusArray() {
|
||||
|
49
src/components/dialogs/HistoryDeleteJobDialog.vue
Normal file
49
src/components/dialogs/HistoryDeleteJobDialog.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<v-dialog :value="show" max-width="400" @keydown.esc="close">
|
||||
<panel :title="$t('History.Delete')" card-class="history-delete-dialog" :margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="close">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text>
|
||||
<p class="mb-0">
|
||||
{{ $t('History.DeleteSingleJobQuestion') }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="" text @click="close">{{ $t('History.Cancel') }}</v-btn>
|
||||
<v-btn color="error" text @click="deleteJob">{{ $t('History.Delete') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { mdiCloseThick } from '@mdi/js'
|
||||
|
||||
@Component
|
||||
export default class HistoryDeleteJobDialog extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
|
||||
@Prop({ type: Boolean, required: true }) show!: boolean
|
||||
@Prop({ type: Object, required: true }) job!: ServerHistoryStateJob
|
||||
|
||||
deleteJob() {
|
||||
this.$socket.emit(
|
||||
'server.history.delete_job',
|
||||
{ uid: this.job.job_id },
|
||||
{ action: 'server/history/getDeletedJobs' }
|
||||
)
|
||||
|
||||
this.close()
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
</script>
|
58
src/components/dialogs/HistoryDeleteSelectedJobsDialog.vue
Normal file
58
src/components/dialogs/HistoryDeleteSelectedJobsDialog.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<v-dialog :value="show" max-width="400" @keydown.esc="close">
|
||||
<panel :title="$t('History.Delete')" card-class="history-delete-selected-dialog" :margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="close">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text>
|
||||
<p class="mb-0">
|
||||
{{ text }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="" text @click="close">{{ $t('History.Cancel') }}</v-btn>
|
||||
<v-btn color="error" text @click="deleteSelectedJobs">{{ $t('History.Delete') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { mdiCloseThick } from '@mdi/js'
|
||||
|
||||
@Component
|
||||
export default class HistoryDeleteJobDialog extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
|
||||
@Prop({ type: Boolean, required: true }) show!: boolean
|
||||
@Prop({ type: Array, required: true }) selectedJobs!: ServerHistoryStateJob[]
|
||||
|
||||
get text() {
|
||||
if (this.selectedJobs.length === 1) return this.$t('History.DeleteSingleJobQuestion')
|
||||
|
||||
return this.$t('History.DeleteSelectedQuestion', { count: this.selectedJobs.length })
|
||||
}
|
||||
|
||||
deleteSelectedJobs() {
|
||||
this.selectedJobs.forEach((item: ServerHistoryStateJob) => {
|
||||
this.$socket.emit(
|
||||
'server.history.delete_job',
|
||||
{ uid: item.job_id },
|
||||
{ action: 'server/history/getDeletedJobs' }
|
||||
)
|
||||
})
|
||||
|
||||
this.$emit('clear-selected-jobs')
|
||||
this.close()
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
</script>
|
170
src/components/dialogs/HistoryDetailsDialog.vue
Normal file
170
src/components/dialogs/HistoryDetailsDialog.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<v-dialog :value="show" :max-width="600" persistent @keydown.esc="close">
|
||||
<panel
|
||||
:title="$t('History.JobDetails')"
|
||||
:icon="mdiUpdate"
|
||||
card-class="history-detail-dialog"
|
||||
:margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="close">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="px-0">
|
||||
<overlay-scrollbars style="height: 350px" class="px-6">
|
||||
<history-details-dialog-entry v-for="field in fields" :key="field.key" :job="job" :field="field" />
|
||||
</overlay-scrollbars>
|
||||
</v-card-text>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { mdiCloseThick, mdiUpdate } from '@mdi/js'
|
||||
import { formatFilesize, formatPrintTime } from '@/plugins/helpers'
|
||||
import { TranslateResult } from 'vue-i18n'
|
||||
|
||||
export interface HistoryDetailsField {
|
||||
key: string
|
||||
label: string | TranslateResult
|
||||
metadata?: boolean
|
||||
unit?: string
|
||||
format?: (value: any) => string | TranslateResult
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class HistoryDetailsDialog extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
mdiUpdate = mdiUpdate
|
||||
|
||||
@Prop({ type: Boolean, required: true }) show!: boolean
|
||||
@Prop({ type: Object, required: true }) job!: ServerHistoryStateJob
|
||||
|
||||
get fields(): HistoryDetailsField[] {
|
||||
return [
|
||||
{
|
||||
key: 'filename',
|
||||
label: this.$t('History.Filename'),
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
label: this.$t('History.Filesize'),
|
||||
metadata: true,
|
||||
format: (value: number) => formatFilesize(value),
|
||||
},
|
||||
{
|
||||
key: 'modified',
|
||||
label: this.$t('History.LastModified'),
|
||||
metadata: true,
|
||||
format: (value: number) => this.formatDateTime(value * 1000),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: this.$t('History.Status'),
|
||||
format: (value: string) =>
|
||||
this.$te(`History.StatusValues.${value}`, 'en') ? this.$t(`History.StatusValues.${value}`) : value,
|
||||
},
|
||||
{
|
||||
key: 'end_time',
|
||||
label: this.$t('History.EndTime'),
|
||||
format: (value: number) => this.formatDateTime(value * 1000),
|
||||
},
|
||||
{
|
||||
key: 'estimated_time',
|
||||
label: this.$t('History.EstimatedTime'),
|
||||
metadata: true,
|
||||
format: (value: number) => formatPrintTime(value),
|
||||
},
|
||||
{
|
||||
key: 'print_duration',
|
||||
label: this.$t('History.PrintDuration'),
|
||||
metadata: true,
|
||||
format: (value: number) => formatPrintTime(value),
|
||||
},
|
||||
{
|
||||
key: 'total_duration',
|
||||
label: this.$t('History.TotalDuration'),
|
||||
metadata: true,
|
||||
format: (value: number) => formatPrintTime(value),
|
||||
},
|
||||
{
|
||||
key: 'filament_weight_total',
|
||||
label: this.$t('History.EstimatedFilamentWeight'),
|
||||
metadata: true,
|
||||
unit: 'g',
|
||||
format: (value: number) => value?.toFixed(2),
|
||||
},
|
||||
{
|
||||
key: 'filament_total',
|
||||
label: this.$t('History.EstimatedFilament'),
|
||||
metadata: true,
|
||||
unit: 'mm',
|
||||
format: (value: number) => value?.toFixed(0),
|
||||
},
|
||||
{
|
||||
key: 'filament_used',
|
||||
label: this.$t('History.FilamentUsed'),
|
||||
metadata: true,
|
||||
unit: 'mm',
|
||||
format: (value: number) => value?.toFixed(0),
|
||||
},
|
||||
{
|
||||
key: 'first_layer_extr_temp',
|
||||
label: this.$t('History.FirstLayerExtTemp'),
|
||||
metadata: true,
|
||||
unit: '°C',
|
||||
},
|
||||
{
|
||||
key: 'first_layer_bed_temp',
|
||||
label: this.$t('History.FirstLayerBedTemp'),
|
||||
metadata: true,
|
||||
unit: '°C',
|
||||
},
|
||||
{
|
||||
key: 'first_layer_height',
|
||||
label: this.$t('History.FirstLayerHeight'),
|
||||
metadata: true,
|
||||
unit: 'mm',
|
||||
},
|
||||
{
|
||||
key: 'layer_height',
|
||||
label: this.$t('History.LayerHeight'),
|
||||
metadata: true,
|
||||
unit: 'mm',
|
||||
},
|
||||
{
|
||||
key: 'object_height',
|
||||
label: this.$t('History.ObjectHeight'),
|
||||
metadata: true,
|
||||
unit: 'mm',
|
||||
},
|
||||
{
|
||||
key: 'slicer',
|
||||
label: this.$t('History.Slicer'),
|
||||
metadata: true,
|
||||
},
|
||||
{
|
||||
key: 'slicer_version',
|
||||
label: this.$t('History.SlicerVersion'),
|
||||
metadata: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
::v-deep .history-details-dialog-entry + .history-details-dialog-entry {
|
||||
margin-top: 1em;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.theme--light ::v-deep .history-details-dialog-entry {
|
||||
border-top-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
</style>
|
41
src/components/dialogs/HistoryDetailsDialogEntry.vue
Normal file
41
src/components/dialogs/HistoryDetailsDialogEntry.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<v-row v-if="show" class="history-details-dialog-entry">
|
||||
<v-col>{{ field.label }}</v-col>
|
||||
<v-col class="text-right">{{ output }}</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { HistoryDetailsField } from '@/components/dialogs/HistoryDetailsDialog.vue'
|
||||
|
||||
@Component
|
||||
export default class HistoryDetailsDialogEntry extends Mixins(BaseMixin) {
|
||||
@Prop({ type: Object, required: true }) job!: ServerHistoryStateJob
|
||||
@Prop({ type: Object, required: true }) field!: HistoryDetailsField
|
||||
|
||||
get show() {
|
||||
return this.value ?? false
|
||||
}
|
||||
|
||||
get value() {
|
||||
const boolMetadata = this.field.metadata ?? false
|
||||
if (!boolMetadata) return this.job[this.field.key]
|
||||
|
||||
const metadata = this.job.metadata ?? null
|
||||
if (metadata === null) return null
|
||||
|
||||
return metadata[this.field.key]
|
||||
}
|
||||
|
||||
get output() {
|
||||
let output = this.value
|
||||
if (this.field.format) output = this.field.format(this.value)
|
||||
|
||||
if (this.field.unit) return `${output} ${this.field.unit}`
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
</script>
|
244
src/components/dialogs/HistoryListPanelAddMaintenance.vue
Normal file
244
src/components/dialogs/HistoryListPanelAddMaintenance.vue
Normal file
@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<v-dialog :value="show" :max-width="600" persistent @keydown.esc="closeDialog">
|
||||
<panel
|
||||
:title="$t('History.AddMaintenance')"
|
||||
:icon="mdiNotebookPlus"
|
||||
card-class="history-add-maintenance-dialog"
|
||||
:margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="closeDialog">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="pb-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
:rules="nameInputRules"
|
||||
:label="$t('History.Name')"
|
||||
hide-details="auto"
|
||||
outlined
|
||||
dense />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea v-model="note" outlined hide-details="auto" :label="$t('History.Note')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<settings-row :title="$t('History.Reminder')">
|
||||
<v-select
|
||||
v-model="reminder"
|
||||
:items="reminderItems"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
class="mt-0" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="reminder">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<settings-row
|
||||
:icon="mdiAdjust"
|
||||
:title="$t('History.FilamentBasedReminder')"
|
||||
:sub-title="$t('History.FilamentBasedReminderDescription')">
|
||||
<v-checkbox v-model="reminderFilament" hide-details class="mt-0" />
|
||||
<v-text-field
|
||||
v-model.number="reminderFilamentValue"
|
||||
hide-details="auto"
|
||||
type="number"
|
||||
class="mt-0"
|
||||
outlined
|
||||
dense
|
||||
:suffix="$t('History.Meter')" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<settings-row
|
||||
:icon="mdiAlarm"
|
||||
:title="$t('History.PrinttimeBasedReminder')"
|
||||
:sub-title="$t('History.PrinttimeBasedReminderDescription')">
|
||||
<v-checkbox v-model="reminderPrinttime" hide-details class="mt-0" />
|
||||
<v-text-field
|
||||
v-model.number="reminderPrinttimeValue"
|
||||
hide-details="auto"
|
||||
type="number"
|
||||
class="mt-0"
|
||||
outlined
|
||||
dense
|
||||
:suffix="$t('History.Hours')" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<settings-row
|
||||
:icon="mdiCalendar"
|
||||
:title="$t('History.DateBasedReminder')"
|
||||
:sub-title="$t('History.DateBasedReminderDescription')">
|
||||
<v-checkbox v-model="reminderDate" hide-details class="mt-0" />
|
||||
<v-text-field
|
||||
v-model.number="reminderDateValue"
|
||||
hide-details="auto"
|
||||
type="number"
|
||||
class="mt-0"
|
||||
outlined
|
||||
dense
|
||||
:suffix="$t('History.Days')" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="closeDialog">{{ $t('History.Cancel') }}</v-btn>
|
||||
<v-btn color="primary" text :disabled="!isValid" @click="save">{{ $t('History.Save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import SettingsRow from '@/components/settings/SettingsRow.vue'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import { mdiAdjust, mdiAlarm, mdiCalendar, mdiCloseThick, mdiNotebookPlus } from '@mdi/js'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Panel,
|
||||
SettingsRow,
|
||||
},
|
||||
})
|
||||
export default class HistoryListPanelAddMaintenance extends Mixins(BaseMixin) {
|
||||
mdiAdjust = mdiAdjust
|
||||
mdiAlarm = mdiAlarm
|
||||
mdiCalendar = mdiCalendar
|
||||
mdiCloseThick = mdiCloseThick
|
||||
mdiNotebookPlus = mdiNotebookPlus
|
||||
|
||||
@Prop({ type: Boolean, default: false }) readonly show!: boolean
|
||||
|
||||
name: string = ''
|
||||
note: string = ''
|
||||
reminder: 'one-time' | 'repeat' | null = null
|
||||
|
||||
reminderFilament: boolean = false
|
||||
reminderFilamentValue: number = 0
|
||||
|
||||
reminderPrinttime: boolean = false
|
||||
reminderPrinttimeValue: number = 0
|
||||
|
||||
reminderDate: boolean = false
|
||||
reminderDateValue: number = 0
|
||||
|
||||
nameInputRules = [(value: string) => !!value || this.$t('History.InvalidNameEmpty')]
|
||||
|
||||
get reminderItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$t('History.NoReminder').toString(),
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
text: this.$t('History.OneTime').toString(),
|
||||
value: 'one-time',
|
||||
},
|
||||
{
|
||||
text: this.$t('History.Repeat').toString(),
|
||||
value: 'repeat',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
get totalFilamentUsed() {
|
||||
return this.$store.state.server.history.job_totals?.total_filament_used ?? 0
|
||||
}
|
||||
|
||||
get totalPrinttime() {
|
||||
return this.$store.state.server.history.job_totals?.total_print_time ?? 0
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
if (this.name === '') return false
|
||||
|
||||
if (this.reminder !== null) {
|
||||
if (!this.reminderFilament && !this.reminderPrinttime && !this.reminderDate) return false
|
||||
|
||||
if (this.reminderFilament && this.reminderFilamentValue <= 0) return false
|
||||
if (this.reminderPrinttime && this.reminderPrinttimeValue <= 0) return false
|
||||
if (this.reminderDate && this.reminderDateValue <= 0) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
save() {
|
||||
const date = new Date()
|
||||
this.$store.dispatch('gui/maintenance/store', {
|
||||
entry: {
|
||||
name: this.name,
|
||||
note: this.note,
|
||||
// divided by 1000 to get seconds, because history entries are also in seconds
|
||||
start_time: date.getTime() / 1000,
|
||||
end_time: null,
|
||||
start_filament: this.totalFilamentUsed,
|
||||
end_filament: null,
|
||||
start_printtime: this.totalPrinttime,
|
||||
end_printtime: null,
|
||||
|
||||
reminder: {
|
||||
type: this.reminder,
|
||||
|
||||
filament: {
|
||||
bool: this.reminderFilament,
|
||||
value: this.reminderFilamentValue,
|
||||
},
|
||||
|
||||
printtime: {
|
||||
bool: this.reminderPrinttime,
|
||||
value: this.reminderPrinttimeValue,
|
||||
},
|
||||
|
||||
date: {
|
||||
bool: this.reminderDate,
|
||||
value: this.reminderDateValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
this.closeDialog()
|
||||
}
|
||||
|
||||
resetValues() {
|
||||
this.name = ''
|
||||
this.note = ''
|
||||
this.reminder = null
|
||||
this.reminderFilament = false
|
||||
this.reminderFilamentValue = 0
|
||||
this.reminderPrinttime = false
|
||||
this.reminderPrinttimeValue = 0
|
||||
this.reminderDate = false
|
||||
this.reminderDateValue = 0
|
||||
}
|
||||
|
||||
@Watch('show')
|
||||
onShowChanged() {
|
||||
if (this.show) this.resetValues()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<v-dialog :value="show" max-width="400">
|
||||
<panel :title="$t('History.Delete')" card-class="history-delete-selected-dialog" :margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="closeDialog">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text>
|
||||
<p class="mb-0">{{ question }}</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="" text @click="closeDialog">{{ $t('History.Cancel') }}</v-btn>
|
||||
<v-btn color="error" text @click="deleteSelectedJobs">{{ $t('History.Delete') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import { mdiCloseThick } from '@mdi/js'
|
||||
import { HistoryListPanelRow } from '@/components/panels/HistoryListPanel.vue'
|
||||
|
||||
@Component({
|
||||
components: { Panel },
|
||||
})
|
||||
export default class HistoryListPanelDeleteSelectedDialog extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
|
||||
@Prop({ type: Boolean, required: true }) readonly show!: boolean
|
||||
|
||||
get selectedJobs() {
|
||||
return this.$store.state.gui.view.history.selectedJobs ?? []
|
||||
}
|
||||
|
||||
set selectedJobs(newVal) {
|
||||
this.$store.dispatch('gui/saveSettingWithoutUpload', { name: 'view.history.selectedJobs', value: newVal })
|
||||
}
|
||||
|
||||
get question() {
|
||||
if (this.selectedJobs.length === 1) return this.$t('History.DeleteSingleJobQuestion')
|
||||
|
||||
return this.$t('History.DeleteSelectedQuestion', { count: this.selectedJobs.length })
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
deleteSelectedJobs() {
|
||||
this.selectedJobs.forEach((item: HistoryListPanelRow) => {
|
||||
if (item.type === 'maintenance') {
|
||||
this.$store.dispatch('gui/maintenance/delete', item.id)
|
||||
return
|
||||
}
|
||||
|
||||
// break if job_id is not present
|
||||
if (!('job_id' in item)) return
|
||||
|
||||
this.$socket.emit(
|
||||
'server.history.delete_job',
|
||||
{ uid: item.job_id },
|
||||
{ action: 'server/history/getDeletedJobs' }
|
||||
)
|
||||
})
|
||||
|
||||
this.selectedJobs = []
|
||||
this.closeDialog()
|
||||
}
|
||||
}
|
||||
</script>
|
130
src/components/dialogs/HistoryListPanelDetailMaintenance.vue
Normal file
130
src/components/dialogs/HistoryListPanelDetailMaintenance.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<v-dialog :value="show" :max-width="500" persistent @keydown.esc="closeDialog">
|
||||
<panel
|
||||
:title="$t('History.Maintenance')"
|
||||
:icon="mdiNotebook"
|
||||
card-class="history-maintenance-dialog"
|
||||
:margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="showEditDialog = true">
|
||||
<v-icon>{{ mdiPencil }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon tile @click="closeDialog">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<overlay-scrollbars style="height: 350px">
|
||||
<v-card-text class="pb-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div>{{ date }}</div>
|
||||
<p class="text-h4 text--primary">{{ item.name }}</p>
|
||||
<div v-if="note" class="text--primary" v-html="note" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-divider class="mt-3 mb-0" />
|
||||
<v-card-text class="pt-0 mb-0 pb-0">
|
||||
<v-timeline align-top dense>
|
||||
<v-timeline-item class="pb-1" small>
|
||||
<strong>{{ outputFirstPointOfHistory }}</strong>
|
||||
</v-timeline-item>
|
||||
<history-list-panel-detail-maintenance-history-entry
|
||||
v-for="entry in history"
|
||||
:key="entry.id"
|
||||
:item="entry"
|
||||
:current="entry.id === item.id"
|
||||
:last="entry.id === history[history.length - 1].id" />
|
||||
</v-timeline>
|
||||
</v-card-text>
|
||||
</overlay-scrollbars>
|
||||
<v-divider class="mt-0" />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="closeDialog">{{ $t('History.Cancel') }}</v-btn>
|
||||
<v-btn v-if="showPerformButton" text color="primary" @click="showPerformDialog = true">
|
||||
{{ $t('History.Perform') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</panel>
|
||||
<history-list-panel-perform-maintenance
|
||||
:show="showPerformDialog"
|
||||
:item="item"
|
||||
@close="showPerformDialog = false"
|
||||
@close-both="closePerform" />
|
||||
<history-list-panel-edit-maintenance :show="showEditDialog" :item="item" @close="showEditDialog = false" />
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import { mdiCloseThick, mdiNotebook, mdiPencil } from '@mdi/js'
|
||||
import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
|
||||
import HistoryListPanelDetailMaintenanceHistoryEntry from '@/components/dialogs/HistoryListPanelDetailMaintenanceHistoryEntry.vue'
|
||||
import HistoryListPanelPerformMaintenance from '@/components/dialogs/HistoryListPanelPerformMaintenance.vue'
|
||||
|
||||
@Component({
|
||||
components: { HistoryListPanelPerformMaintenance, Panel, HistoryListPanelDetailMaintenanceHistoryEntry },
|
||||
})
|
||||
export default class HistoryListPanelDetailMaintenance extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
mdiNotebook = mdiNotebook
|
||||
mdiPencil = mdiPencil
|
||||
|
||||
@Prop({ type: Boolean, default: false }) readonly show!: boolean
|
||||
@Prop({ type: Object, default: false }) readonly item!: GuiMaintenanceStateEntry
|
||||
|
||||
showEditDialog = false
|
||||
showPerformDialog = false
|
||||
|
||||
get date() {
|
||||
return this.formatDateTime(this.item.start_time * 1000, false)
|
||||
}
|
||||
|
||||
get note() {
|
||||
return this.item.note.replaceAll('\n', '<br>')
|
||||
}
|
||||
|
||||
get showPerformButton() {
|
||||
if (this.item.end_time) return false
|
||||
|
||||
return this.item.reminder?.type ?? false
|
||||
}
|
||||
|
||||
get allEntries() {
|
||||
return this.$store.getters['gui/maintenance/getEntries'] ?? []
|
||||
}
|
||||
|
||||
get history() {
|
||||
const array = []
|
||||
|
||||
let latest_entry_id = this.item.id
|
||||
while (latest_entry_id) {
|
||||
const entry = this.allEntries.find((entry: GuiMaintenanceStateEntry) => entry.id === latest_entry_id)
|
||||
if (!entry) break
|
||||
array.push(entry)
|
||||
latest_entry_id = entry.last_entry
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
get outputFirstPointOfHistory() {
|
||||
if (this.item.reminder.type === null) return this.$t('History.EntrySince')
|
||||
if (this.item.end_time === null) return this.$t('History.EntryNextPerform')
|
||||
|
||||
return this.$t('History.EntryPerformedAt', { date: this.formatDateTime(this.item.end_time * 1000) })
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
closePerform() {
|
||||
this.showPerformDialog = false
|
||||
this.closeDialog()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-timeline-item class="pb-2" small hide-dot>
|
||||
<div>
|
||||
<span v-if="restFilamentText" :class="restFilamentClass">
|
||||
<v-icon small>{{ mdiAdjust }}</v-icon>
|
||||
{{ restFilamentText }}
|
||||
</span>
|
||||
<span v-if="restPrinttimeText" :class="restPrinttimeClass">
|
||||
<v-icon small>{{ mdiAlarm }}</v-icon>
|
||||
{{ restPrinttimeText }}
|
||||
</span>
|
||||
<span v-if="restDaysText" :class="restDaysClass">
|
||||
<v-icon small>{{ mdiCalendar }}</v-icon>
|
||||
{{ restDaysText }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="note" class="mt-2 mb-0" v-html="note" />
|
||||
</v-timeline-item>
|
||||
<v-timeline-item :class="classDateItem" small>
|
||||
<strong>{{ dateText }}</strong>
|
||||
</v-timeline-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import { mdiAdjust, mdiAlarm, mdiCalendar, mdiCloseThick } from '@mdi/js'
|
||||
import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
|
||||
|
||||
@Component({
|
||||
components: { Panel },
|
||||
})
|
||||
export default class HistoryListPanelDetailMaintenanceHistoryEntry extends Mixins(BaseMixin) {
|
||||
mdiAdjust = mdiAdjust
|
||||
mdiAlarm = mdiAlarm
|
||||
mdiCalendar = mdiCalendar
|
||||
mdiCloseThick = mdiCloseThick
|
||||
|
||||
@Prop({ type: Object, default: false }) readonly item!: GuiMaintenanceStateEntry
|
||||
@Prop({ type: Boolean, default: false }) readonly current!: boolean
|
||||
@Prop({ type: Boolean, default: false }) readonly last!: boolean
|
||||
|
||||
get date() {
|
||||
return this.formatDateTime(this.item.start_time * 1000, false)
|
||||
}
|
||||
|
||||
get dateText() {
|
||||
if (this.last) return this.$t('History.EntryCreatedAt', { date: this.date })
|
||||
|
||||
return this.$t('History.EntryPerformedAt', { date: this.date })
|
||||
}
|
||||
|
||||
get showGoals() {
|
||||
if (this.item.reminder.type === null) return false
|
||||
|
||||
return this.current && this.item.end_time === null
|
||||
}
|
||||
|
||||
get restFilament() {
|
||||
const start = this.item?.start_filament ?? 0
|
||||
const end = this.item.end_filament ?? 0
|
||||
const current = this.$store.state.server.history.job_totals?.total_filament_used ?? 0
|
||||
|
||||
// calc filament since start
|
||||
// if end is not null, calc used filament until end
|
||||
let used = current - start
|
||||
if (end) used = end - start
|
||||
|
||||
// convert to m
|
||||
used /= 1000
|
||||
|
||||
return used
|
||||
}
|
||||
|
||||
get restFilamentText() {
|
||||
const value = this.item.reminder.filament?.value ?? 0
|
||||
if (!this.showGoals) return `${this.restFilament.toFixed(0)} m`
|
||||
|
||||
if (!this.item.reminder.filament.bool) return false
|
||||
|
||||
return `${this.restFilament.toFixed(0)} / ${value} m`
|
||||
}
|
||||
|
||||
get restFilamentClass() {
|
||||
const output = ['mr-3']
|
||||
if (!this.showGoals || !this.item.reminder.filament.bool) return output
|
||||
|
||||
const value = this.item.reminder.filament?.value ?? 0
|
||||
if (this.restFilament > value) return [...output, 'error--text', 'font-weight-bold']
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
get restPrinttime() {
|
||||
const start = this.item.start_printtime ?? 0
|
||||
const end = this.item.end_printtime ?? 0
|
||||
const current = this.$store.state.server.history.job_totals?.total_print_time ?? 0
|
||||
|
||||
// calc filament since start
|
||||
// if end is not null, calc used filament until end
|
||||
let used = current - start
|
||||
if (end) used = end - start
|
||||
|
||||
// convert to h
|
||||
used /= 3600
|
||||
|
||||
return used
|
||||
}
|
||||
|
||||
get restPrinttimeText() {
|
||||
const value = this.item.reminder.printtime?.value ?? 0
|
||||
if (!this.showGoals) return `${this.restPrinttime.toFixed(1)} h`
|
||||
|
||||
if (!this.item.reminder.printtime.bool) return false
|
||||
|
||||
return `${this.restPrinttime.toFixed(1)} / ${value} h`
|
||||
}
|
||||
|
||||
get restPrinttimeClass() {
|
||||
const output = ['mr-3']
|
||||
if (!this.showGoals || !this.item.reminder.printtime.bool) return output
|
||||
|
||||
const value = this.item.reminder.printtime?.value ?? 0
|
||||
if (this.restPrinttime > value) return [...output, 'error--text', 'font-weight-bold']
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
get restDays() {
|
||||
const start = this.item.start_time ?? 0
|
||||
const end = this.item.end_time ?? 0
|
||||
const current = new Date().getTime() / 1000
|
||||
|
||||
// calc days since start
|
||||
// if end is not null, calc used days until end
|
||||
let used = current - start
|
||||
if (end) used = end - start
|
||||
|
||||
return used / (60 * 60 * 24)
|
||||
}
|
||||
|
||||
get restDaysText() {
|
||||
const value = this.item.reminder.date?.value ?? 0
|
||||
|
||||
if (!this.showGoals) return `${this.restDays.toFixed(0)} days`
|
||||
|
||||
if (!this.item.reminder.date.bool) return false
|
||||
|
||||
return `${this.restDays.toFixed(0)} / ${value} days`
|
||||
}
|
||||
|
||||
get restDaysClass() {
|
||||
const output = ['mr-3']
|
||||
if (!this.showGoals || !this.item.reminder.date.bool) return output
|
||||
|
||||
const value = this.item.reminder.date?.value ?? 0
|
||||
if (this.restDays > value) return [...output, 'error--text', 'font-weight-bold']
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
get classDateItem() {
|
||||
return {
|
||||
'pb-2': !this.last,
|
||||
'pb-5': this.last,
|
||||
}
|
||||
}
|
||||
|
||||
get note() {
|
||||
return this.item.perform_note?.replaceAll('\n', '<br>')
|
||||
}
|
||||
}
|
||||
</script>
|
126
src/components/dialogs/HistoryListPanelDetailsDialog.vue
Normal file
126
src/components/dialogs/HistoryListPanelDetailsDialog.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" :max-width="600" persistent @keydown.esc="closeDialog">
|
||||
<panel
|
||||
:title="$t('History.JobDetails').toString()"
|
||||
:icon="mdiUpdate"
|
||||
card-class="history-detail-dialog"
|
||||
:margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="closeDialog">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="pa-0">
|
||||
<overlay-scrollbars style="height: 350px" class="px-6">
|
||||
<template v-for="(entry, index) in entries">
|
||||
<v-divider v-if="index > 0" :key="'history_detail_entry_divider_' + index" class="my-3" />
|
||||
<v-row :key="'history_detail_entry_' + index">
|
||||
<v-col>{{ entry.name }}</v-col>
|
||||
<v-col class="text-right">{{ entry.value }}</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</overlay-scrollbars>
|
||||
</v-card-text>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import SettingsRow from '@/components/settings/SettingsRow.vue'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { mdiCloseThick, mdiUpdate } from '@mdi/js'
|
||||
import { formatFilesize, formatPrintTime } from '@/plugins/helpers'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Panel,
|
||||
SettingsRow,
|
||||
},
|
||||
})
|
||||
export default class HistoryListPanelDetailsDialog extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
mdiUpdate = mdiUpdate
|
||||
|
||||
formatFilesize = formatFilesize
|
||||
formatPrintTime = formatPrintTime
|
||||
|
||||
@Prop({ type: Boolean, required: true }) readonly show!: boolean
|
||||
@Prop({ type: Object, required: true }) readonly job!: ServerHistoryStateJob
|
||||
|
||||
get entries() {
|
||||
let entries: { name: string; value: string | null; exists: boolean }[] = [
|
||||
{
|
||||
name: this.$t('History.Filename').toString(),
|
||||
value: this.job.filename,
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.Filesize').toString(),
|
||||
value: formatFilesize(this.job.metadata?.filesize ?? 0),
|
||||
exists: (this.job.metadata?.filesize ?? 0) > 0,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.LastModified').toString(),
|
||||
value: this.formatDateTime((this.job.metadata?.modified ?? 0) * 1000),
|
||||
exists: (this.job.metadata?.modified ?? 0) > 0,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.Status').toString(),
|
||||
value: this.$te(`History.StatusValues.${this.job.status}`, 'en')
|
||||
? this.$t(`History.StatusValues.${this.job.status}`).toString()
|
||||
: this.job.status,
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.StartTime').toString(),
|
||||
value: this.formatDateTime(this.job.start_time * 1000),
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.EndTime').toString(),
|
||||
value: this.formatDateTime(this.job.end_time * 1000),
|
||||
exists: this.job.end_time > 0,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.EstimatedTime').toString(),
|
||||
value: this.formatPrintTime(this.job.metadata?.estimated_time ?? 0),
|
||||
exists: this.job.metadata && 'estimated_time' in this.job.metadata,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.PrintDuration').toString(),
|
||||
value: this.formatPrintTime(this.job.print_duration ?? 0),
|
||||
exists: this.job.print_duration > 0,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.TotalDuration').toString(),
|
||||
value: this.formatPrintTime(this.job.total_duration ?? 0),
|
||||
exists: this.job.total_duration > 0,
|
||||
},
|
||||
{
|
||||
name: this.$t('History.EstimatedFilamentWeight').toString(),
|
||||
value: `${Math.round((this.job.metadata?.filament_weight_total ?? 0) * 100) / 100} g`,
|
||||
exists: this.job.metadata && 'filament_weight_total' in this.job.metadata,
|
||||
},
|
||||
]
|
||||
|
||||
return entries.filter((entry) => entry.exists)
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.$emit('close-dialog')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep .os-content .row:first-child {
|
||||
margin-top: 1em !important;
|
||||
}
|
||||
|
||||
::v-deep .os-content .row:last-child {
|
||||
margin-bottom: 1em !important;
|
||||
}
|
||||
</style>
|
251
src/components/dialogs/HistoryListPanelEditMaintenance.vue
Normal file
251
src/components/dialogs/HistoryListPanelEditMaintenance.vue
Normal file
@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<v-dialog :value="show" :max-width="600" persistent @keydown.esc="closeDialog">
|
||||
<panel
|
||||
:title="$t('History.EditMaintenance')"
|
||||
:icon="mdiNotebook"
|
||||
card-class="history-edit-maintenance-dialog"
|
||||
:margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="closeDialog">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="pb-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
:rules="nameInputRules"
|
||||
:label="$t('History.Name')"
|
||||
hide-details="auto"
|
||||
outlined
|
||||
dense />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea v-model="note" outlined hide-details="auto" :label="$t('History.Note')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<settings-row :title="$t('History.Reminder')">
|
||||
<v-select
|
||||
v-model="reminder"
|
||||
:items="reminderItems"
|
||||
:disabled="item.end_time !== null"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
class="mt-0" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="reminder">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<settings-row
|
||||
:icon="mdiAdjust"
|
||||
:title="$t('History.FilamentBasedReminder')"
|
||||
:sub-title="$t('History.FilamentBasedReminderDescription')">
|
||||
<v-checkbox
|
||||
v-model="reminderFilament"
|
||||
:disabled="item.end_time !== null"
|
||||
hide-details
|
||||
class="mt-0" />
|
||||
<v-text-field
|
||||
v-model.number="reminderFilamentValue"
|
||||
:disabled="item.end_time !== null"
|
||||
hide-details="auto"
|
||||
type="number"
|
||||
class="mt-0"
|
||||
outlined
|
||||
dense
|
||||
:suffix="$t('History.Meter')" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<settings-row
|
||||
:icon="mdiAlarm"
|
||||
:title="$t('History.PrinttimeBasedReminder')"
|
||||
:sub-title="$t('History.PrinttimeBasedReminderDescription')">
|
||||
<v-checkbox
|
||||
v-model="reminderPrinttime"
|
||||
:disabled="item.end_time !== null"
|
||||
hide-details
|
||||
class="mt-0" />
|
||||
<v-text-field
|
||||
v-model.number="reminderPrinttimeValue"
|
||||
:disabled="item.end_time !== null"
|
||||
hide-details="auto"
|
||||
type="number"
|
||||
class="mt-0"
|
||||
outlined
|
||||
dense
|
||||
:suffix="$t('History.Hours')" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<settings-row
|
||||
:icon="mdiCalendar"
|
||||
:title="$t('History.DateBasedReminder')"
|
||||
:sub-title="$t('History.DateBasedReminderDescription')">
|
||||
<v-checkbox
|
||||
v-model="reminderDate"
|
||||
:disabled="item.end_time !== null"
|
||||
hide-details
|
||||
class="mt-0" />
|
||||
<v-text-field
|
||||
v-model.number="reminderDateValue"
|
||||
:disabled="item.end_time !== null"
|
||||
hide-details="auto"
|
||||
type="number"
|
||||
class="mt-0"
|
||||
outlined
|
||||
dense
|
||||
:suffix="$t('History.Days')" />
|
||||
</settings-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="closeDialog">{{ $t('History.Cancel') }}</v-btn>
|
||||
<v-btn color="primary" text :disabled="!isValid" @click="save">{{ $t('History.Save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import SettingsRow from '@/components/settings/SettingsRow.vue'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import { mdiAdjust, mdiAlarm, mdiCalendar, mdiCloseThick, mdiNotebook } from '@mdi/js'
|
||||
import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Panel,
|
||||
SettingsRow,
|
||||
},
|
||||
})
|
||||
export default class HistoryListPanelAddMaintenance extends Mixins(BaseMixin) {
|
||||
mdiAdjust = mdiAdjust
|
||||
mdiAlarm = mdiAlarm
|
||||
mdiCalendar = mdiCalendar
|
||||
mdiCloseThick = mdiCloseThick
|
||||
mdiNotebook = mdiNotebook
|
||||
|
||||
@Prop({ type: Boolean, default: false }) readonly show!: boolean
|
||||
@Prop({ type: Object, required: true }) readonly item!: GuiMaintenanceStateEntry
|
||||
|
||||
name: string = ''
|
||||
note: string = ''
|
||||
reminder: 'one-time' | 'repeat' | null = null
|
||||
|
||||
reminderFilament: boolean = false
|
||||
reminderFilamentValue: number = 0
|
||||
|
||||
reminderPrinttime: boolean = false
|
||||
reminderPrinttimeValue: number = 0
|
||||
|
||||
reminderDate: boolean = false
|
||||
reminderDateValue: number = 0
|
||||
|
||||
nameInputRules = [(value: string) => !!value || this.$t('History.InvalidNameEmpty')]
|
||||
|
||||
get reminderItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$t('History.NoReminder').toString(),
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
text: this.$t('History.OneTime').toString(),
|
||||
value: 'one-time',
|
||||
},
|
||||
{
|
||||
text: this.$t('History.Repeat').toString(),
|
||||
value: 'repeat',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
get totalFilamentUsed() {
|
||||
return this.$store.state.server.history.job_totals?.total_filament_used ?? 0
|
||||
}
|
||||
|
||||
get totalPrinttime() {
|
||||
return this.$store.state.server.history.job_totals?.total_print_time ?? 0
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
if (this.name === '') return false
|
||||
|
||||
if (this.reminder !== null) {
|
||||
if (!this.reminderFilament && !this.reminderPrinttime && !this.reminderDate) return false
|
||||
|
||||
if (this.reminderFilament && this.reminderFilamentValue <= 0) return false
|
||||
if (this.reminderPrinttime && this.reminderPrinttimeValue <= 0) return false
|
||||
if (this.reminderDate && this.reminderDateValue <= 0) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
save() {
|
||||
const item = { ...this.item }
|
||||
// Remove type from item, this is not needed and comes from the history list
|
||||
// @ts-ignore
|
||||
if ('type' in item) delete item.type
|
||||
|
||||
item.name = this.name
|
||||
item.note = this.note
|
||||
item.reminder = {
|
||||
type: this.reminder,
|
||||
filament: {
|
||||
bool: this.reminderFilament,
|
||||
value: this.reminderFilamentValue,
|
||||
},
|
||||
printtime: {
|
||||
bool: this.reminderPrinttime,
|
||||
value: this.reminderPrinttimeValue,
|
||||
},
|
||||
date: {
|
||||
bool: this.reminderDate,
|
||||
value: this.reminderDateValue,
|
||||
},
|
||||
}
|
||||
|
||||
this.$store.dispatch('gui/maintenance/update', item)
|
||||
|
||||
this.closeDialog()
|
||||
}
|
||||
|
||||
@Watch('show')
|
||||
onShowChanged() {
|
||||
if (this.show) {
|
||||
this.name = this.item.name
|
||||
this.note = this.item.note
|
||||
this.reminder = this.item.reminder?.type ?? null
|
||||
this.reminderFilament = this.item.reminder?.filament.bool ?? false
|
||||
this.reminderFilamentValue = this.item.reminder?.filament.value ?? 0
|
||||
this.reminderPrinttime = this.item.reminder?.printtime.bool ?? false
|
||||
this.reminderPrinttimeValue = this.item.reminder?.printtime.value ?? 0
|
||||
this.reminderDate = this.item.reminder?.date.bool ?? false
|
||||
this.reminderDateValue = this.item.reminder?.date.value ?? 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
88
src/components/dialogs/HistoryListPanelNoteDialog.vue
Normal file
88
src/components/dialogs/HistoryListPanelNoteDialog.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<v-dialog v-model="show" :max-width="600" persistent @keydown.esc="closeDialog">
|
||||
<panel :title="panelTitle" :icon="icon" card-class="history-note-dialog" :margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="closeDialog">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="pb-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea v-model="note" outlined hide-details :label="$t('History.Note')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="" text @click="closeDialog">{{ $t('History.Cancel') }}</v-btn>
|
||||
<v-btn color="primary" text @click="saveNote">{{ $t('History.Save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import SettingsRow from '@/components/settings/SettingsRow.vue'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { mdiCloseThick, mdiNoteEditOutline, mdiNotePlusOutline } from '@mdi/js'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Panel,
|
||||
SettingsRow,
|
||||
},
|
||||
})
|
||||
export default class HistoryListPanelNoteDialog extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
|
||||
note: string = ''
|
||||
|
||||
@Prop({ type: Boolean, required: true }) readonly show!: boolean
|
||||
@Prop({ type: String, required: true }) readonly type!: 'create' | 'edit'
|
||||
@Prop({ type: Object, required: true }) readonly job!: ServerHistoryStateJob
|
||||
|
||||
get panelTitle() {
|
||||
if (this.type === 'create') return this.$t('History.CreateNote').toString()
|
||||
|
||||
return this.$t('History.EditNote').toString()
|
||||
}
|
||||
|
||||
get icon() {
|
||||
if (this.type === 'create') return mdiNotePlusOutline
|
||||
|
||||
return mdiNoteEditOutline
|
||||
}
|
||||
|
||||
saveNote() {
|
||||
this.$store.dispatch('server/history/saveHistoryNote', {
|
||||
job_id: this.job?.job_id,
|
||||
note: this.note,
|
||||
})
|
||||
|
||||
this.closeDialog()
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.$emit('close-dialog')
|
||||
}
|
||||
|
||||
@Watch('show', { immediate: true })
|
||||
onShowChanged() {
|
||||
if (this.show) this.note = this.job.note ?? ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep .os-content .row:first-child {
|
||||
margin-top: 1em !important;
|
||||
}
|
||||
|
||||
::v-deep .os-content .row:last-child {
|
||||
margin-bottom: 1em !important;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<v-dialog :value="show" :max-width="400" persistent @keydown.esc="closeDialog">
|
||||
<panel
|
||||
:title="$t('History.PerformMaintenance')"
|
||||
:icon="mdiNotebook"
|
||||
card-class="history-perform-maintenance-dialog"
|
||||
:margin-bottom="false">
|
||||
<template #buttons>
|
||||
<v-btn icon tile @click="closeDialog">
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="pb-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea v-model="note" outlined hide-details="auto" :label="$t('History.AddANote')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="closeDialog">{{ $t('History.Cancel') }}</v-btn>
|
||||
<v-btn v-if="showPerformButton" text color="primary" @click="perform">{{ performButtonText }}</v-btn>
|
||||
</v-card-actions>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import { mdiCloseThick, mdiNotebook } from '@mdi/js'
|
||||
import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
|
||||
import HistoryListPanelDetailMaintenanceHistoryEntry from '@/components/dialogs/HistoryListPanelDetailMaintenanceHistoryEntry.vue'
|
||||
|
||||
@Component({
|
||||
components: { Panel, HistoryListPanelDetailMaintenanceHistoryEntry },
|
||||
})
|
||||
export default class HistoryListPanelPerformMaintenance extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
mdiNotebook = mdiNotebook
|
||||
|
||||
@Prop({ type: Boolean, default: false }) readonly show!: boolean
|
||||
@Prop({ type: Object, default: false }) readonly item!: GuiMaintenanceStateEntry
|
||||
|
||||
note: string = ''
|
||||
|
||||
get showPerformButton() {
|
||||
if (this.item.end_time) return false
|
||||
|
||||
return this.item.reminder?.type ?? false
|
||||
}
|
||||
|
||||
get performButtonText() {
|
||||
if (this.item.reminder?.type === 'repeat') return this.$t('History.PerformedAndReschedule')
|
||||
|
||||
return this.$t('History.Performed')
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
perform() {
|
||||
this.$store.dispatch('gui/maintenance/perform', { id: this.item.id, note: this.note })
|
||||
this.$emit('close-both')
|
||||
}
|
||||
|
||||
@Watch('show')
|
||||
onShowChanged(show: boolean) {
|
||||
if (show) this.note = ''
|
||||
}
|
||||
}
|
||||
</script>
|
@ -29,7 +29,7 @@ export default class SettingsGeneralDatabase extends BaseMixin {
|
||||
},
|
||||
{
|
||||
value: 'navigation',
|
||||
label: this.$t('Settings.GeneralTab.DBNavigation'),
|
||||
label: this.$t('Settings.GeneralTab.DbNavigation'),
|
||||
},
|
||||
{
|
||||
value: 'uiSettings',
|
||||
@ -115,6 +115,14 @@ export default class SettingsGeneralDatabase extends BaseMixin {
|
||||
backupableNamespaces = backupableNamespaces.sort(this.sortNamespaces)
|
||||
}
|
||||
|
||||
// add maintenance if exists
|
||||
if (availableNamespaces.includes('maintenance')) {
|
||||
backupableNamespaces.push({
|
||||
value: 'maintenance',
|
||||
label: this.$t('Settings.GeneralTab.DbMaintenance'),
|
||||
})
|
||||
}
|
||||
|
||||
// add timelapse if exists
|
||||
if (availableNamespaces.includes('timelapse')) {
|
||||
backupableNamespaces.push({
|
||||
|
@ -1,98 +1,80 @@
|
||||
<template>
|
||||
<v-alert :class="`notification-menu-entry--priority-${entry.priority}`" text :color="alertColor" border="left">
|
||||
<v-row align="start">
|
||||
<v-col class="grow">
|
||||
<v-row align="start" class="flex-nowrap">
|
||||
<v-col class="grow pb-2">
|
||||
<div class="notification-menu-entry__headline mb-1 text-subtitle-1">
|
||||
<template v-if="'url' in entry">
|
||||
<a :class="`text-decoration-none ${alertColor}--text`" :href="entry.url" target="_blank">
|
||||
<v-icon small :class="`${alertColor}--text pb-1`">
|
||||
{{ mdiLinkVariant }}
|
||||
</v-icon>
|
||||
{{ entry.title }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span :class="`${alertColor}--text`">{{ entry.title }}</span>
|
||||
</template>
|
||||
<a
|
||||
v-if="'url' in entry"
|
||||
:class="`text-decoration-none ${alertColor}--text `"
|
||||
:href="entry.url"
|
||||
target="_blank">
|
||||
<v-icon small :class="`${alertColor}--text pb-1`">
|
||||
{{ mdiLinkVariant }}
|
||||
</v-icon>
|
||||
{{ entry.title }}
|
||||
</a>
|
||||
<span v-else :class="`${alertColor}--text`">{{ entry.title }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="notification-menu-entry__description text-body-2 mb-0 text--disabled font-weight-light"
|
||||
v-html="formatedText"></p>
|
||||
v-html="formatedText" />
|
||||
<v-btn
|
||||
v-if="entryType === 'maintenance'"
|
||||
outlined
|
||||
small
|
||||
:color="alertColor"
|
||||
class="mt-3 mb-0 w-100"
|
||||
@click="showMaintenanceDetails = true">
|
||||
{{ $t('App.Notifications.ShowDetails') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="entry.priority !== 'critical'"
|
||||
class="shrink pl-0 pb-0 pt-1 pr-2 d-flex flex-column align-self-stretch justify-space-between">
|
||||
<v-btn v-if="entryType === 'announcement'" icon plain :color="alertColor" class="mb-2" @click="close">
|
||||
class="shrink pl-0 pb-1 pt-1 pr-2 d-flex flex-column align-self-stretch justify-space-between">
|
||||
<v-btn
|
||||
v-if="entryType !== 'maintenance'"
|
||||
icon
|
||||
plain
|
||||
:color="alertColor"
|
||||
class="mb-2"
|
||||
@click="xButtonAction">
|
||||
<v-icon>{{ mdiClose }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else icon plain :color="alertColor" class="mb-2" @click="dismiss('reboot', null)">
|
||||
<v-icon>{{ mdiClose }}</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon plain retain-focus-on-click :color="alertColor" class="pb-1" @click="expand = !expand">
|
||||
<v-spacer />
|
||||
<v-btn icon plain retain-focus-on-click :color="alertColor" @click="expand = !expand">
|
||||
<v-icon>{{ mdiBellOffOutline }}</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="entry.priority !== 'critical'">
|
||||
<v-expand-transition>
|
||||
<div v-show="expand" class="pt-1" style="width: 100%">
|
||||
<v-divider class="pb-1 ml-2"></v-divider>
|
||||
<div v-show="expand" class="pt-1 w-100">
|
||||
<v-divider class="pb-1 ml-2" />
|
||||
<div class="text-right py-1" style="font-size: 0.875rem">
|
||||
<span class="text--disabled text-caption font-weight-light">
|
||||
{{ $t('App.Notifications.Remind') }}
|
||||
</span>
|
||||
<template v-if="entryType === 'announcement'">
|
||||
<v-btn
|
||||
:color="alertColor"
|
||||
x-small
|
||||
plain
|
||||
text
|
||||
outlined
|
||||
class="mx-1"
|
||||
@click="dismiss('time', 60 * 60)">
|
||||
1H
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:color="alertColor"
|
||||
x-small
|
||||
plain
|
||||
text
|
||||
outlined
|
||||
class="mx-1"
|
||||
@click="dismiss('time', 60 * 60 * 24)">
|
||||
1D
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:color="alertColor"
|
||||
x-small
|
||||
plain
|
||||
text
|
||||
outlined
|
||||
class="mx-1"
|
||||
@click="dismiss('time', 60 * 60 * 24 * 7)">
|
||||
7D
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn
|
||||
:color="alertColor"
|
||||
x-small
|
||||
plain
|
||||
text
|
||||
outlined
|
||||
class="mx-1"
|
||||
@click="dismiss('reboot', null)">
|
||||
{{ $t('App.Notifications.NextReboot') }}
|
||||
</v-btn>
|
||||
<v-btn :color="alertColor" x-small plain text outlined class="mx-1" @click="close">
|
||||
{{ $t('App.Notifications.Never') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-btn
|
||||
v-for="reminder in reminderTimes"
|
||||
:key="reminder.text"
|
||||
:color="alertColor"
|
||||
x-small
|
||||
plain
|
||||
text
|
||||
outlined
|
||||
class="mx-1"
|
||||
@click="reminder.clickFunction">
|
||||
{{ reminder.text }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</v-row>
|
||||
<history-list-panel-detail-maintenance
|
||||
v-if="entryType === 'maintenance'"
|
||||
:show="showMaintenanceDetails"
|
||||
:item="maintenanceEntry"
|
||||
@close="showMaintenanceDetails = false" />
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
@ -101,6 +83,13 @@ import BaseMixin from '@/components/mixins/base'
|
||||
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
|
||||
import { mdiClose, mdiLinkVariant, mdiBellOffOutline } from '@mdi/js'
|
||||
import { GuiNotificationStateEntry } from '@/store/gui/notifications/types'
|
||||
import { TranslateResult } from 'vue-i18n'
|
||||
import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
|
||||
|
||||
interface ReminderOption {
|
||||
text: string | TranslateResult
|
||||
clickFunction: Function
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {},
|
||||
@ -110,7 +99,8 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
|
||||
mdiLinkVariant = mdiLinkVariant
|
||||
mdiBellOffOutline = mdiBellOffOutline
|
||||
|
||||
private expand = false
|
||||
expand = false
|
||||
showMaintenanceDetails = false
|
||||
|
||||
@Prop({ required: true })
|
||||
declare readonly entry: GuiNotificationStateEntry
|
||||
@ -139,6 +129,49 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
|
||||
return this.entry.id.slice(0, posFirstSlash)
|
||||
}
|
||||
|
||||
get maintenanceEntry() {
|
||||
if (this.entryType !== 'maintenance') return null
|
||||
|
||||
const id = this.entry.id.replace('maintenance/', '')
|
||||
const entries = this.$store.getters['gui/maintenance/getEntries']
|
||||
|
||||
return entries.find((entry: GuiMaintenanceStateEntry) => entry.id === id)
|
||||
}
|
||||
|
||||
get reminderTimes() {
|
||||
let output: ReminderOption[] = [
|
||||
{
|
||||
text: this.$t('App.Notifications.NextReboot'),
|
||||
clickFunction: () => this.dismiss('reboot', null),
|
||||
},
|
||||
{ text: this.$t('App.Notifications.Never'), clickFunction: () => this.close() },
|
||||
]
|
||||
|
||||
if (['announcement', 'maintenance'].includes(this.entryType)) {
|
||||
output = []
|
||||
output.push({
|
||||
text: this.$t('App.Notifications.OneHourShort'),
|
||||
clickFunction: () => this.dismiss('time', 60 * 60),
|
||||
})
|
||||
output.push({
|
||||
text: this.$t('App.Notifications.OneDayShort'),
|
||||
clickFunction: () => this.dismiss('time', 60 * 60 * 24),
|
||||
})
|
||||
output.push({
|
||||
text: this.$t('App.Notifications.OneWeekShort'),
|
||||
clickFunction: () => this.dismiss('time', 60 * 60 * 24 * 7),
|
||||
})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
xButtonAction() {
|
||||
if (this.entryType === 'announcement') return this.close()
|
||||
|
||||
this.dismiss('reboot', null)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$store.dispatch('gui/notifications/close', { id: this.entry.id })
|
||||
}
|
||||
@ -157,10 +190,12 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
|
||||
<style scoped>
|
||||
.notification-menu-entry__headline {
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.notification-menu-entry__description {
|
||||
max-width: 292px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.notification-menu-entry--priority-critical .notification-menu-entry__description {
|
||||
|
@ -34,9 +34,9 @@
|
||||
</v-card-text>
|
||||
</overlay-scrollbars>
|
||||
<template v-if="notifications.length > 1">
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn text color="primary" class="mr-2" @click="dismissAll">
|
||||
<v-icon left>{{ mdiCloseBoxMultipleOutline }}</v-icon>
|
||||
{{ $t('App.Notifications.DismissAll') }}
|
||||
@ -44,11 +44,9 @@
|
||||
</v-card-actions>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-card-text class="text-center">
|
||||
<span class="text-disabled">{{ $t('App.Notifications.NoNotification') }}</span>
|
||||
</v-card-text>
|
||||
</template>
|
||||
<v-card-text v-else class="text-center">
|
||||
<span class="text-disabled">{{ $t('App.Notifications.NoNotification') }}</span>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
@ -68,7 +66,7 @@ export default class TheNotificationMenu extends Mixins(BaseMixin) {
|
||||
mdiBellOutline = mdiBellOutline
|
||||
mdiCloseBoxMultipleOutline = mdiCloseBoxMultipleOutline
|
||||
|
||||
private boolMenu = false
|
||||
boolMenu = false
|
||||
|
||||
get notifications() {
|
||||
return this.$store.getters['gui/notifications/getNotifications'] ?? []
|
||||
@ -97,9 +95,9 @@ export default class TheNotificationMenu extends Mixins(BaseMixin) {
|
||||
this.notifications.forEach(async (entry: GuiNotificationStateEntry) => {
|
||||
if (entry.id.startsWith('announcement')) {
|
||||
await this.$store.dispatch('gui/notifications/close', { id: entry.id })
|
||||
} else {
|
||||
await this.$store.dispatch('gui/notifications/dismiss', { id: entry.id, type: 'reboot', time: null })
|
||||
}
|
||||
|
||||
await this.$store.dispatch('gui/notifications/dismiss', { id: entry.id, type: 'reboot', time: null })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -675,7 +675,6 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
|
||||
mdiDragVertical = mdiDragVertical
|
||||
|
||||
formatFilesize = formatFilesize
|
||||
formatPrintTime = formatPrintTime
|
||||
sortFiles = sortFiles
|
||||
|
||||
declare $refs: {
|
||||
@ -1458,32 +1457,32 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) {
|
||||
outputValue(col: any, item: FileStateGcodefile) {
|
||||
const value = col.value in item ? item[col.value] : null
|
||||
|
||||
if (value !== null) {
|
||||
switch (col.outputType) {
|
||||
case 'filesize':
|
||||
return formatFilesize(value)
|
||||
if (value === null) return '--'
|
||||
|
||||
case 'date':
|
||||
return this.formatDateTime(value)
|
||||
switch (col.outputType) {
|
||||
case 'filesize':
|
||||
return formatFilesize(value)
|
||||
|
||||
case 'time':
|
||||
return this.formatPrintTime(value)
|
||||
case 'date':
|
||||
return this.formatDateTime(value)
|
||||
|
||||
case 'temp':
|
||||
return value.toFixed() + ' °C'
|
||||
case 'time':
|
||||
return formatPrintTime(value)
|
||||
|
||||
case 'length':
|
||||
if (value > 1000) return (value / 1000).toFixed(2) + ' m'
|
||||
case 'temp':
|
||||
return value.toFixed() + ' °C'
|
||||
|
||||
return value.toFixed(2) + ' mm'
|
||||
case 'length':
|
||||
if (value > 1000) return (value / 1000).toFixed(2) + ' m'
|
||||
|
||||
case 'weight':
|
||||
return value.toFixed(2) + ' g'
|
||||
return value.toFixed(2) + ' mm'
|
||||
|
||||
default:
|
||||
return value
|
||||
}
|
||||
} else return '--'
|
||||
case 'weight':
|
||||
return value.toFixed(2) + ' g'
|
||||
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
330
src/components/panels/History/HistoryListEntryJob.vue
Normal file
330
src/components/panels/History/HistoryListEntryJob.vue
Normal file
@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<tr
|
||||
:key="item.job_id"
|
||||
v-longpress:600="(e) => showContextMenu(e)"
|
||||
:class="cssClasses"
|
||||
@contextmenu="showContextMenu($event)"
|
||||
@click="detailsDialogBool = true">
|
||||
<td class="pr-0">
|
||||
<v-simple-checkbox v-ripple :value="isSelected" class="pa-0 mr-0" @click.stop="select(!isSelected)" />
|
||||
</td>
|
||||
<td class="px-0 text-center" style="width: 32px">
|
||||
<template v-if="!item.exists">
|
||||
<v-icon class="text--disabled">{{ mdiFileCancel }}</v-icon>
|
||||
</template>
|
||||
<template v-else-if="smallThumbnail && bigThumbnail">
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<vue-load-image>
|
||||
<img
|
||||
slot="image"
|
||||
:alt="item.filename"
|
||||
:src="smallThumbnail"
|
||||
width="32"
|
||||
height="32"
|
||||
v-bind="attrs"
|
||||
v-on="on" />
|
||||
<div slot="preloader">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
</div>
|
||||
<div slot="error">
|
||||
<v-icon>{{ mdiFile }}</v-icon>
|
||||
</div>
|
||||
</vue-load-image>
|
||||
</template>
|
||||
<span><img :alt="item.filename" :src="bigThumbnail" width="250" /></span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template v-else-if="smallThumbnail">
|
||||
<vue-load-image>
|
||||
<img slot="image" :alt="item.filename" :src="smallThumbnail" width="32" height="32" />
|
||||
<div slot="preloader">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
</div>
|
||||
<div slot="error">
|
||||
<v-icon>{{ mdiFile }}</v-icon>
|
||||
</div>
|
||||
</vue-load-image>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon>{{ mdiFile }}</v-icon>
|
||||
</template>
|
||||
</td>
|
||||
<td>{{ item.filename }}</td>
|
||||
<td class="text-right text-no-wrap">
|
||||
<template v-if="'note' in item && item.note">
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon small class="mr-2" v-bind="attrs" v-on="on">
|
||||
{{ mdiNoteTextOutline }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span v-html="item.note.replaceAll('\n', '<br />')" />
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<span v-bind="attrs" v-on="on">
|
||||
<v-icon small :color="statusColor" :disabled="!item.exists">
|
||||
{{ statusIcon }}
|
||||
</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ statusName }}</span>
|
||||
</v-tooltip>
|
||||
</td>
|
||||
<td v-for="col in tableFields" :key="col.value" class="text-no-wrap" v-html="outputValue(col, item)" />
|
||||
<!-- Context menu -->
|
||||
<v-menu v-model="contextMenuBool" :position-x="contextMenuX" :position-y="contextMenuY" absolute offset-y>
|
||||
<v-list>
|
||||
<v-list-item @click="detailsDialogBool = true">
|
||||
<v-icon class="mr-1">{{ mdiTextBoxSearch }}</v-icon>
|
||||
{{ $t('History.Details') }}
|
||||
</v-list-item>
|
||||
<v-list-item v-if="item.note" @click="editNote">
|
||||
<v-icon class="mr-1">{{ mdiNoteEditOutline }}</v-icon>
|
||||
{{ $t('History.EditNote') }}
|
||||
</v-list-item>
|
||||
<v-list-item v-else @click="createNote">
|
||||
<v-icon class="mr-1">{{ mdiNotePlusOutline }}</v-icon>
|
||||
{{ $t('History.AddNote') }}
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="item.exists"
|
||||
:disabled="printerIsPrinting || !klipperReadyForGui"
|
||||
@click="startPrint">
|
||||
<v-icon class="mr-1">{{ mdiPrinter }}</v-icon>
|
||||
{{ $t('History.Reprint') }}
|
||||
</v-list-item>
|
||||
<v-list-item v-if="item.exists && isJobQueueAvailable" @click="addToQueue">
|
||||
<v-icon class="mr-1">{{ mdiPlaylistPlus }}</v-icon>
|
||||
{{ $t('Files.AddToQueue') }}
|
||||
</v-list-item>
|
||||
<v-list-item v-if="item.exists && isJobQueueAvailable" @click="addBatchToQueueDialogBool = true">
|
||||
<v-icon class="mr-1">{{ mdiPlaylistPlus }}</v-icon>
|
||||
{{ $t('Files.AddBatchToQueue') }}
|
||||
</v-list-item>
|
||||
<v-list-item class="red--text" @click="deleteJob">
|
||||
<v-icon class="mr-1" color="error">{{ mdiDelete }}</v-icon>
|
||||
{{ $t('History.Delete') }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<!-- details dialog -->
|
||||
<history-list-panel-details-dialog
|
||||
:show="detailsDialogBool"
|
||||
:job="item"
|
||||
@close-dialog="detailsDialogBool = false" />
|
||||
<!-- create/edit note dialog -->
|
||||
<history-list-panel-note-dialog
|
||||
:show="noteDialogBool"
|
||||
:type="noteDialogType"
|
||||
:job="item"
|
||||
@close-dialog="noteDialogBool = false" />
|
||||
<!-- add to queue dialog -->
|
||||
<add-batch-to-queue-dialog
|
||||
:is-visible="addBatchToQueueDialogBool"
|
||||
:show-toast="true"
|
||||
:filename="item.filename"
|
||||
@close="addBatchToQueueDialogBool = false" />
|
||||
</tr>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import HistoryListPanelDetailsDialog from '@/components/dialogs/HistoryListPanelDetailsDialog.vue'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { thumbnailBigMin, thumbnailSmallMax, thumbnailSmallMin } from '@/store/variables'
|
||||
import {
|
||||
mdiCloseThick,
|
||||
mdiDelete,
|
||||
mdiFile,
|
||||
mdiFileCancel,
|
||||
mdiNoteEditOutline,
|
||||
mdiNotePlusOutline,
|
||||
mdiNoteTextOutline,
|
||||
mdiPlaylistPlus,
|
||||
mdiPrinter,
|
||||
mdiTextBoxSearch,
|
||||
} from '@mdi/js'
|
||||
import { formatFilesize, formatPrintTime } from '@/plugins/helpers'
|
||||
import { HistoryListPanelCol } from '@/components/panels/HistoryListPanel.vue'
|
||||
import HistoryListPanelNoteDialog from '@/components/dialogs/HistoryListPanelNoteDialog.vue'
|
||||
import AddBatchToQueueDialog from '@/components/dialogs/AddBatchToQueueDialog.vue'
|
||||
|
||||
@Component({
|
||||
components: { AddBatchToQueueDialog, HistoryListPanelNoteDialog, HistoryListPanelDetailsDialog, Panel },
|
||||
})
|
||||
export default class HistoryListPanel extends Mixins(BaseMixin) {
|
||||
mdiCloseThick = mdiCloseThick
|
||||
mdiDelete = mdiDelete
|
||||
mdiFile = mdiFile
|
||||
mdiFileCancel = mdiFileCancel
|
||||
mdiNoteEditOutline = mdiNoteEditOutline
|
||||
mdiNotePlusOutline = mdiNotePlusOutline
|
||||
mdiNoteTextOutline = mdiNoteTextOutline
|
||||
mdiPrinter = mdiPrinter
|
||||
mdiTextBoxSearch = mdiTextBoxSearch
|
||||
mdiPlaylistPlus = mdiPlaylistPlus
|
||||
|
||||
detailsDialogBool = false
|
||||
|
||||
contextMenuBool = false
|
||||
contextMenuX = 0
|
||||
contextMenuY = 0
|
||||
|
||||
noteDialogBool = false
|
||||
noteDialogType: 'create' | 'edit' = 'create'
|
||||
|
||||
addBatchToQueueDialogBool = false
|
||||
|
||||
@Prop({ type: Object, required: true }) readonly item!: ServerHistoryStateJob
|
||||
@Prop({ type: Array, required: true }) readonly tableFields!: HistoryListPanelCol[]
|
||||
@Prop({ type: Boolean, required: true }) readonly isSelected!: boolean
|
||||
|
||||
get smallThumbnail() {
|
||||
if ((this.item.metadata?.thumbnails?.length ?? 0) < 1) return false
|
||||
|
||||
const thumbnail = this.item.metadata?.thumbnails?.find(
|
||||
(thumb: any) =>
|
||||
thumb.width >= thumbnailSmallMin &&
|
||||
thumb.width <= thumbnailSmallMax &&
|
||||
thumb.height >= thumbnailSmallMin &&
|
||||
thumb.height <= thumbnailSmallMax
|
||||
)
|
||||
|
||||
let relative_url = ''
|
||||
if (this.item.filename.lastIndexOf('/') !== -1) {
|
||||
relative_url = this.item.filename.substring(0, this.item.filename.lastIndexOf('/'))
|
||||
}
|
||||
|
||||
if ((thumbnail?.relative_path ?? null) === null) return false
|
||||
|
||||
return `${this.apiUrl}/server/files/gcodes/${encodeURI(relative_url + thumbnail?.relative_path)}?timestamp=${
|
||||
this.item.metadata.modified
|
||||
}`
|
||||
}
|
||||
|
||||
get bigThumbnail() {
|
||||
if ((this.item.metadata?.thumbnails?.length ?? 0) < 1) return false
|
||||
|
||||
const thumbnail = this.item.metadata?.thumbnails?.find((thumb: any) => thumb.width >= thumbnailBigMin)
|
||||
|
||||
let relative_url = ''
|
||||
if (this.item.filename.lastIndexOf('/') !== -1) {
|
||||
relative_url = this.item.filename.substring(0, this.item.filename.lastIndexOf('/') + 1)
|
||||
}
|
||||
|
||||
if ((thumbnail?.relative_path ?? null) === null) return false
|
||||
|
||||
return `${this.apiUrl}/server/files/gcodes/${encodeURI(relative_url + thumbnail?.relative_path)}?timestamp=${
|
||||
this.item.metadata.modified
|
||||
}`
|
||||
}
|
||||
|
||||
get statusIcon() {
|
||||
return this.$store.getters['server/history/getPrintStatusIcon'](this.item.status)
|
||||
}
|
||||
|
||||
get statusColor() {
|
||||
return this.$store.getters['server/history/getPrintStatusIconColor'](this.item.status)
|
||||
}
|
||||
|
||||
get statusName() {
|
||||
// check if translation exists
|
||||
if (!this.$t(`History.StatusValues.${this.item.status}`, 'en')) return this.item.status.replace(/_/g, ' ')
|
||||
|
||||
return this.$t(`History.StatusValues.${this.item.status}`)
|
||||
}
|
||||
|
||||
get cssClasses() {
|
||||
let output = ['file-list-cursor', 'user-select-none']
|
||||
|
||||
if (!this.item.exists) output.push('text--disabled')
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
get isJobQueueAvailable() {
|
||||
return this.moonrakerComponents.includes('job_queue')
|
||||
}
|
||||
|
||||
select(newVal: boolean) {
|
||||
this.$emit('select', newVal)
|
||||
}
|
||||
|
||||
showContextMenu(e: any) {
|
||||
e?.preventDefault()
|
||||
if (this.contextMenuBool) return
|
||||
|
||||
this.contextMenuBool = true
|
||||
this.contextMenuX = e?.clientX || e?.pageX || window.screenX / 2
|
||||
this.contextMenuY = e?.clientY || e?.pageY || window.screenY / 2
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.contextMenuBool = true
|
||||
})
|
||||
}
|
||||
|
||||
startPrint() {
|
||||
if (!this.item.exists) return
|
||||
|
||||
this.$socket.emit('printer.print.start', { filename: this.item.filename }, { action: 'switchToDashboard' })
|
||||
}
|
||||
|
||||
createNote() {
|
||||
this.noteDialogType = 'create'
|
||||
this.noteDialogBool = true
|
||||
}
|
||||
|
||||
editNote() {
|
||||
this.noteDialogType = 'edit'
|
||||
this.noteDialogBool = true
|
||||
}
|
||||
|
||||
addToQueue() {
|
||||
this.$store.dispatch('server/jobQueue/addToQueue', [this.item.filename])
|
||||
this.$toast.info(this.$t('History.AddToQueueSuccessful', { filename: this.item.filename }).toString())
|
||||
}
|
||||
|
||||
deleteJob() {
|
||||
this.$socket.emit(
|
||||
'server.history.delete_job',
|
||||
{ uid: this.item.job_id },
|
||||
{ action: 'server/history/getDeletedJobs' }
|
||||
)
|
||||
}
|
||||
|
||||
outputValue(col: HistoryListPanelCol, item: ServerHistoryStateJob) {
|
||||
//@ts-ignore
|
||||
let value = col.value in item ? item[col.value] : null
|
||||
if (value === null) value = col.value in item.metadata ? item.metadata[col.value] : null
|
||||
if (value === null) return '--'
|
||||
|
||||
if (col.value === 'slicer') value += '<br />' + item.metadata.slicer_version
|
||||
|
||||
switch (col.outputType) {
|
||||
case 'filesize':
|
||||
return formatFilesize(value)
|
||||
|
||||
case 'date':
|
||||
return this.formatDateTime(value * 1000)
|
||||
|
||||
case 'time':
|
||||
return formatPrintTime(value, false)
|
||||
|
||||
case 'temp':
|
||||
return value?.toFixed() + ' °C'
|
||||
|
||||
case 'length':
|
||||
if (value > 1000) return (value / 1000).toFixed(2) + ' m'
|
||||
|
||||
return value?.toFixed(2) + ' mm'
|
||||
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
210
src/components/panels/History/HistoryListEntryMaintenance.vue
Normal file
210
src/components/panels/History/HistoryListEntryMaintenance.vue
Normal file
@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<tr
|
||||
:key="item.id"
|
||||
v-longpress:600="(e) => showContextMenu(e)"
|
||||
:class="cssClasses"
|
||||
@contextmenu="showContextMenu($event)"
|
||||
@click="detailsDialogBool = true">
|
||||
<td class="pr-0">
|
||||
<v-simple-checkbox v-ripple :value="isSelected" class="pa-0 mr-0" @click.stop="select(!isSelected)" />
|
||||
</td>
|
||||
<td class="px-0 text-center" style="width: 32px">
|
||||
<v-icon color="primary">{{ icon }}</v-icon>
|
||||
</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td class="text-right text-no-wrap">
|
||||
<v-tooltip v-if="reminder !== null" top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon small color="primary" v-bind="attrs" v-on="on">
|
||||
{{ alarmIcon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<div>
|
||||
<div v-if="restTextFilament">
|
||||
<v-icon small class="mr-1">{{ mdiAdjust }}</v-icon>
|
||||
{{ restTextFilament }}
|
||||
</div>
|
||||
<div v-if="restTextPrinttime">
|
||||
<v-icon small class="mr-1">{{ mdiAlarm }}</v-icon>
|
||||
{{ restTextPrinttime }}
|
||||
</div>
|
||||
<div v-if="restTextDays">
|
||||
<v-icon small class="mr-1">{{ mdiCalendar }}</v-icon>
|
||||
{{ restTextDays }}
|
||||
</div>
|
||||
</div>
|
||||
</v-tooltip>
|
||||
</td>
|
||||
<td class="text-left text-no-wrap">
|
||||
{{ formatDateTime(item.start_time * 1000, false) }}
|
||||
</td>
|
||||
<td :colspan="tableFields.length - 1" />
|
||||
<v-menu v-model="contextMenuBool" :position-x="contextMenuX" :position-y="contextMenuY" absolute offset-y>
|
||||
<v-list>
|
||||
<v-list-item @click="detailsDialogBool = true">
|
||||
<v-icon class="mr-1">{{ mdiTextBoxSearch }}</v-icon>
|
||||
{{ $t('History.Details') }}
|
||||
</v-list-item>
|
||||
<v-list-item class="red--text" @click="deleteEntry">
|
||||
<v-icon class="mr-1" color="error">{{ mdiDelete }}</v-icon>
|
||||
{{ $t('History.Delete') }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<history-list-panel-detail-maintenance
|
||||
:show="detailsDialogBool"
|
||||
:item="item"
|
||||
@close="detailsDialogBool = false" />
|
||||
</tr>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import {
|
||||
mdiAdjust,
|
||||
mdiAlarm,
|
||||
mdiAlarmMultiple,
|
||||
mdiCalendar,
|
||||
mdiDelete,
|
||||
mdiNotebook,
|
||||
mdiNotebookCheck,
|
||||
mdiTextBoxSearch,
|
||||
} from '@mdi/js'
|
||||
import { HistoryListPanelCol } from '@/components/panels/HistoryListPanel.vue'
|
||||
import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
|
||||
import HistoryListPanelDetailMaintenance from '@/components/dialogs/HistoryListPanelDetailMaintenance.vue'
|
||||
|
||||
@Component({
|
||||
components: { HistoryListPanelDetailMaintenance, Panel },
|
||||
})
|
||||
export default class HistoryListPanel extends Mixins(BaseMixin) {
|
||||
mdiAdjust = mdiAdjust
|
||||
mdiAlarm = mdiAlarm
|
||||
mdiCalendar = mdiCalendar
|
||||
mdiDelete = mdiDelete
|
||||
mdiTextBoxSearch = mdiTextBoxSearch
|
||||
|
||||
detailsDialogBool = false
|
||||
|
||||
contextMenuBool = false
|
||||
contextMenuX = 0
|
||||
contextMenuY = 0
|
||||
|
||||
@Prop({ type: Object, required: true }) readonly item!: GuiMaintenanceStateEntry
|
||||
@Prop({ type: Array, required: true }) readonly tableFields!: HistoryListPanelCol[]
|
||||
@Prop({ type: Boolean, required: true }) readonly isSelected!: boolean
|
||||
|
||||
get cssClasses() {
|
||||
let output = ['file-list-cursor', 'user-select-none']
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
get restFilament() {
|
||||
const start = this.item?.start_filament ?? 0
|
||||
const end = this.item.end_filament ?? 0
|
||||
const current = this.$store.state.server.history.job_totals?.total_filament_used ?? 0
|
||||
|
||||
// calc filament since start
|
||||
// if end is not null, calc used filament until end
|
||||
let used = current - start
|
||||
if (end) used = end - start
|
||||
|
||||
// convert to m
|
||||
used /= 1000
|
||||
|
||||
return used
|
||||
}
|
||||
|
||||
get restTextFilament() {
|
||||
if (!this.item.reminder.filament.bool) return false
|
||||
|
||||
const value = this.item.reminder.filament?.value ?? 0
|
||||
|
||||
return `${this.restFilament.toFixed(0)} / ${value} m`
|
||||
}
|
||||
|
||||
get restPrinttime() {
|
||||
const start = this.item.start_printtime ?? 0
|
||||
const end = this.item.end_printtime ?? 0
|
||||
const current = this.$store.state.server.history.job_totals?.total_print_time ?? 0
|
||||
|
||||
// calc filament since start
|
||||
// if end is not null, calc used filament until end
|
||||
let used = current - start
|
||||
if (end) used = end - start
|
||||
|
||||
// convert to h
|
||||
used /= 3600
|
||||
|
||||
return used
|
||||
}
|
||||
|
||||
get restTextPrinttime() {
|
||||
if (!this.item.reminder.printtime.bool) return false
|
||||
|
||||
const value = this.item.reminder.printtime?.value ?? 0
|
||||
|
||||
return `${this.restPrinttime.toFixed(1)} / ${value} h`
|
||||
}
|
||||
|
||||
get restDays() {
|
||||
const start = this.item.start_time ?? 0
|
||||
const end = this.item.end_time ?? 0
|
||||
const current = new Date().getTime() / 1000
|
||||
|
||||
// calc days since start
|
||||
// if end is not null, calc used days until end
|
||||
let used = current - start
|
||||
if (end) used = end - start
|
||||
|
||||
return used / (60 * 60 * 24)
|
||||
}
|
||||
|
||||
get restTextDays() {
|
||||
if (!this.item.reminder.date.bool) return false
|
||||
|
||||
const value = this.item.reminder.date?.value ?? 0
|
||||
|
||||
return `${this.restDays.toFixed(0)} / ${value} days`
|
||||
}
|
||||
|
||||
get reminder() {
|
||||
return this.item.reminder?.type ?? null
|
||||
}
|
||||
|
||||
get alarmIcon() {
|
||||
if (this.reminder === 'repeat') return mdiAlarmMultiple
|
||||
|
||||
return mdiAlarm
|
||||
}
|
||||
|
||||
get icon() {
|
||||
if (this.item.end_time !== null) return mdiNotebookCheck
|
||||
|
||||
return mdiNotebook
|
||||
}
|
||||
|
||||
select(newVal: boolean) {
|
||||
this.$emit('select', newVal)
|
||||
}
|
||||
|
||||
showContextMenu(e: any) {
|
||||
e?.preventDefault()
|
||||
if (this.contextMenuBool) return
|
||||
|
||||
this.contextMenuBool = true
|
||||
this.contextMenuX = e?.clientX || e?.pageX || window.screenX / 2
|
||||
this.contextMenuY = e?.clientY || e?.pageY || window.screenY / 2
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.contextMenuBool = true
|
||||
})
|
||||
}
|
||||
|
||||
deleteEntry() {
|
||||
this.$store.dispatch('gui/maintenance/delete', this.item.id)
|
||||
}
|
||||
}
|
||||
</script>
|
143
src/components/panels/History/HistoryListPanelExportCsv.vue
Normal file
143
src/components/panels/History/HistoryListPanelExportCsv.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn class="px-2 minwidth-0 ml-3" v-bind="attrs" v-on="on" @click="exportHistory">
|
||||
<v-icon>{{ mdiDatabaseExportOutline }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t('History.TitleExportHistory') }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { formatFilesize } from '@/plugins/helpers'
|
||||
import { mdiDatabaseExportOutline } from '@mdi/js'
|
||||
import { HistoryListPanelCol } from '@/components/panels/HistoryListPanel.vue'
|
||||
|
||||
@Component
|
||||
export default class HistoryListPanelExportCsv extends Mixins(BaseMixin) {
|
||||
formatFilesize = formatFilesize
|
||||
mdiDatabaseExportOutline = mdiDatabaseExportOutline
|
||||
|
||||
@Prop({ type: Array, required: true }) headers!: HistoryListPanelCol[]
|
||||
@Prop({ type: Array, required: true }) tableFields!: HistoryListPanelCol[]
|
||||
|
||||
get jobs() {
|
||||
return this.$store.getters['server/history/getFilteredJobList'] ?? []
|
||||
}
|
||||
|
||||
get selectedJobs() {
|
||||
return this.$store.state.gui.view.history.selectedJobs ?? []
|
||||
}
|
||||
|
||||
get existsSlicerCol() {
|
||||
return this.headers.find((header) => header.value === 'slicer')?.visible ?? false
|
||||
}
|
||||
|
||||
exportHistory() {
|
||||
const checkString = parseFloat('1.23').toLocaleString(this.browserLocale)
|
||||
const decimalSeparator = checkString.indexOf(',') >= 0 ? ',' : '.'
|
||||
const csvSeperator = decimalSeparator === ',' ? ';' : ','
|
||||
|
||||
const content: string[][] = []
|
||||
content.push(this.createHeaderRow())
|
||||
content.push(...this.createContentRows(csvSeperator))
|
||||
|
||||
// escape fields with the csvSeperator in the content
|
||||
// prettier-ignore
|
||||
const csvContent =
|
||||
'data:text/csv;charset=utf-8,' +
|
||||
content.map((entry) =>
|
||||
entry.map((field) => (field.indexOf(csvSeperator) === -1 ? field : `"${field}"`)).join(csvSeperator)
|
||||
).join('\n')
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.setAttribute('href', encodeURI(csvContent))
|
||||
link.setAttribute('download', 'print_history.csv')
|
||||
document.body.appendChild(link)
|
||||
|
||||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
|
||||
private createHeaderRow() {
|
||||
const row: string[] = []
|
||||
|
||||
row.push('filename')
|
||||
row.push('status')
|
||||
|
||||
this.tableFields.forEach((col) => {
|
||||
row.push(col.value)
|
||||
})
|
||||
|
||||
if (this.existsSlicerCol) row.push('slicer')
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
get exportJobs() {
|
||||
if (this.selectedJobs.length) return [...this.selectedJobs]
|
||||
|
||||
return [...this.jobs]
|
||||
}
|
||||
|
||||
private createContentRows(csvSeperator: string) {
|
||||
if (this.exportJobs.length === 0) return []
|
||||
|
||||
const rows: string[][] = []
|
||||
|
||||
this.exportJobs.forEach((job: ServerHistoryStateJob) => {
|
||||
const row: string[] = []
|
||||
|
||||
if (job.filename.includes(csvSeperator)) row.push(`"${job.filename}"`)
|
||||
else row.push(job.filename)
|
||||
row.push(job.status)
|
||||
|
||||
this.tableFields.forEach((col) => {
|
||||
row.push(this.outputValue(col, job, csvSeperator))
|
||||
})
|
||||
|
||||
if (this.existsSlicerCol) {
|
||||
const slicer = job.metadata?.slicer ?? '--'
|
||||
const slicer_version = job.metadata?.slicer_version ?? '--'
|
||||
|
||||
row.push(`${slicer} ${slicer_version}`)
|
||||
}
|
||||
|
||||
rows.push(row)
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
private outputValue(col: any, job: any, escapeChar: string) {
|
||||
let value = col.value in job ? job[col.value] : null
|
||||
if (value === null) value = col.value in job.metadata ? job.metadata[col.value] : null
|
||||
|
||||
switch (col.outputType) {
|
||||
case 'date':
|
||||
return this.formatDateTime(value * 1000)
|
||||
|
||||
case 'time':
|
||||
return value?.toFixed() ?? ''
|
||||
|
||||
default:
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
return value?.toLocaleString(this.browserLocale, { useGrouping: false }) ?? 0
|
||||
|
||||
case 'string':
|
||||
if (escapeChar !== null && value.includes(escapeChar)) value = '"' + value + '"'
|
||||
|
||||
return value
|
||||
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
226
src/components/panels/History/HistoryListRow.vue
Normal file
226
src/components/panels/History/HistoryListRow.vue
Normal file
@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<tr
|
||||
v-longpress:600="(e) => showContextMenu(e)"
|
||||
:class="trClasses"
|
||||
@contextmenu="showContextMenu($event)"
|
||||
@click="clickRow">
|
||||
<td class="pr-0">
|
||||
<v-simple-checkbox v-ripple :value="isSelected" class="pa-0 mr-0" @click.stop="select(!isSelected)" />
|
||||
</td>
|
||||
<td class="px-0 text-center" style="width: 32px">
|
||||
<v-icon v-if="!job.exists" class="text--disabled">{{ mdiFileCancel }}</v-icon>
|
||||
<v-tooltip v-else-if="smallThumbnail" top :disabled="!bigThumbnail">
|
||||
<template #activator="{ on, attrs }">
|
||||
<vue-load-image>
|
||||
<img
|
||||
slot="image"
|
||||
:src="smallThumbnail"
|
||||
width="32"
|
||||
height="32"
|
||||
:alt="job.filename"
|
||||
v-bind="attrs"
|
||||
v-on="on" />
|
||||
<div slot="preloader">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
</div>
|
||||
<div slot="error">
|
||||
<v-icon>{{ mdiFile }}</v-icon>
|
||||
</div>
|
||||
</vue-load-image>
|
||||
</template>
|
||||
<span><img :src="bigThumbnail" width="250" :alt="job.filename" /></span>
|
||||
</v-tooltip>
|
||||
<v-icon v-else>{{ mdiFile }}</v-icon>
|
||||
</td>
|
||||
<td>{{ job.filename }}</td>
|
||||
<td class="text-right text-no-wrap">
|
||||
<template v-if="job.note ?? false">
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon small class="mr-2" v-bind="attrs" v-on="on">
|
||||
{{ mdiNotebook }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span v-html="job.note.replaceAll('\n', '<br />')" />
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<span v-bind="attrs" v-on="on">
|
||||
<v-icon small :color="statusColor" :disabled="!job.exists">{{ statusIcon }}</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ statusText }}</span>
|
||||
</v-tooltip>
|
||||
</td>
|
||||
<history-list-row-cell v-for="col in tableFields" :key="col.value" :job="job" :col="col" />
|
||||
<td v-if="existsSlicerCol" class=" ">
|
||||
{{ job.metadata?.slicer ?? '--' }}
|
||||
<small v-if="job.metadata?.slicer_version ?? false">
|
||||
<br />
|
||||
{{ job.metadata?.slicer_version }}
|
||||
</small>
|
||||
</td>
|
||||
<v-menu v-model="contextMenuShown" :position-x="contextMenuX" :position-y="contextMenuY" absolute offset-y>
|
||||
<v-list>
|
||||
<v-list-item @click="clickRow">
|
||||
<v-icon class="mr-1">{{ mdiTextBoxSearch }}</v-icon>
|
||||
{{ $t('History.Details') }}
|
||||
</v-list-item>
|
||||
<v-list-item v-if="job.note ?? false" @click="showNoteDialog = true">
|
||||
<v-icon class="mr-1">{{ mdiNotebookEdit }}</v-icon>
|
||||
{{ $t('History.EditNote') }}
|
||||
</v-list-item>
|
||||
<v-list-item v-else @click="showNoteDialog = true">
|
||||
<v-icon class="mr-1">{{ mdiNotebookPlus }}</v-icon>
|
||||
{{ $t('History.AddNote') }}
|
||||
</v-list-item>
|
||||
<v-list-item v-if="job.exists" :disabled="printerIsPrinting || !klipperReadyForGui" @click="startPrint">
|
||||
<v-icon class="mr-1">{{ mdiPrinter }}</v-icon>
|
||||
{{ $t('History.Reprint') }}
|
||||
</v-list-item>
|
||||
<v-list-item class="red--text" @click="showDeleteDialog = true">
|
||||
<v-icon class="mr-1" color="error">{{ mdiDelete }}</v-icon>
|
||||
{{ $t('History.Delete') }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<history-details-dialog :show="showDetailsDialog" :job="job" @close="showDetailsDialog = false" />
|
||||
<history-note-dialog :show="showNoteDialog" :job="job" @close="showNoteDialog = false" />
|
||||
<history-delete-job-dialog :show="showDeleteDialog" :job="job" @close="showDeleteDialog = false" />
|
||||
</tr>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { HistoryListPanelCol } from '@/components/panels/HistoryListPanel.vue'
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiFile,
|
||||
mdiFileCancel,
|
||||
mdiNotebook,
|
||||
mdiNotebookEdit,
|
||||
mdiNotebookPlus,
|
||||
mdiPrinter,
|
||||
mdiTextBoxSearch,
|
||||
} from '@mdi/js'
|
||||
import { thumbnailBigMin, thumbnailSmallMax, thumbnailSmallMin } from '@/store/variables'
|
||||
import HistoryListRowCell from '@/components/panels/History/HistoryListRowCell.vue'
|
||||
import HistoryDetailsDialog from '@/components/dialogs/HistoryDetailsDialog.vue'
|
||||
|
||||
@Component({
|
||||
components: { HistoryDetailsDialog, HistoryListRowCell },
|
||||
})
|
||||
export default class HistoryListRow extends Mixins(BaseMixin) {
|
||||
mdiDelete = mdiDelete
|
||||
mdiFile = mdiFile
|
||||
mdiFileCancel = mdiFileCancel
|
||||
mdiNotebook = mdiNotebook
|
||||
mdiNotebookEdit = mdiNotebookEdit
|
||||
mdiNotebookPlus = mdiNotebookPlus
|
||||
mdiPrinter = mdiPrinter
|
||||
mdiTextBoxSearch = mdiTextBoxSearch
|
||||
|
||||
@Prop({ type: Object, required: true }) job!: ServerHistoryStateJob
|
||||
@Prop({ type: Array, required: true }) tableFields!: HistoryListPanelCol[]
|
||||
@Prop({ type: Boolean, required: true }) isSelected!: boolean
|
||||
@Prop({ type: Boolean, required: true }) existsSlicerCol!: boolean
|
||||
|
||||
contextMenuShown = false
|
||||
contextMenuX = 0
|
||||
contextMenuY = 0
|
||||
|
||||
showDetailsDialog = false
|
||||
showDeleteDialog = false
|
||||
showNoteDialog = false
|
||||
|
||||
get trClasses() {
|
||||
return {
|
||||
'file-list-cursor': true,
|
||||
'user-select-none': true,
|
||||
'text--disabled': !this.job.exists,
|
||||
}
|
||||
}
|
||||
|
||||
get thumbnails() {
|
||||
return this.job.metadata?.thumbnails ?? []
|
||||
}
|
||||
|
||||
get smallThumbnail() {
|
||||
const thumbnail = this.thumbnails.find(
|
||||
(thumb: any) =>
|
||||
thumb.width >= thumbnailSmallMin &&
|
||||
thumb.width <= thumbnailSmallMax &&
|
||||
thumb.height >= thumbnailSmallMin &&
|
||||
thumb.height <= thumbnailSmallMax
|
||||
)
|
||||
if (!thumbnail) return false
|
||||
|
||||
let relative_url = ''
|
||||
if (this.job.filename.lastIndexOf('/') !== -1) {
|
||||
relative_url = this.job.filename.substring(0, this.job.filename.lastIndexOf('/') + 1)
|
||||
}
|
||||
|
||||
return `${this.apiUrl}/server/files/gcodes/${encodeURI(relative_url + thumbnail.relative_path)}?timestamp=${this
|
||||
.job.metadata?.modified}`
|
||||
}
|
||||
|
||||
get bigThumbnail() {
|
||||
const thumbnail = this.thumbnails.find((thumb: any) => thumb.width >= thumbnailBigMin)
|
||||
if (!thumbnail) return false
|
||||
|
||||
let relative_url = ''
|
||||
if (this.job.filename.lastIndexOf('/') !== -1) {
|
||||
relative_url = this.job.filename.substring(0, this.job.filename.lastIndexOf('/') + 1)
|
||||
}
|
||||
|
||||
return `${this.apiUrl}/server/files/gcodes/${encodeURI(relative_url + thumbnail.relative_path)}?timestamp=${this
|
||||
.job.metadata?.modified}`
|
||||
}
|
||||
|
||||
get statusColor() {
|
||||
return this.$store.getters['server/history/getPrintStatusIconColor'](this.job.status)
|
||||
}
|
||||
|
||||
get statusIcon() {
|
||||
return this.$store.getters['server/history/getPrintStatusIcon'](this.job.status)
|
||||
}
|
||||
|
||||
get statusText() {
|
||||
// If the translation exists, use it
|
||||
if (this.$te(`History.StatusValues.${this.job.status}`, 'en')) {
|
||||
return this.$t(`History.StatusValues.${this.job.status}`)
|
||||
}
|
||||
|
||||
// fallback uses the status name
|
||||
return this.job.status.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
showContextMenu(e: MouseEvent) {
|
||||
if (!this.contextMenuShown) {
|
||||
e?.preventDefault()
|
||||
this.contextMenuX = e?.clientX || e?.pageX || window.screenX / 2
|
||||
this.contextMenuY = e?.clientY || e?.pageY || window.screenY / 2
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.contextMenuShown = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
select(value: boolean) {
|
||||
this.$emit('select', value)
|
||||
}
|
||||
|
||||
clickRow() {
|
||||
this.showDetailsDialog = true
|
||||
}
|
||||
|
||||
startPrint() {
|
||||
if (!this.job.exists) return
|
||||
|
||||
this.$socket.emit('printer.print.start', { filename: this.job.filename }, { action: 'switchToDashboard' })
|
||||
}
|
||||
}
|
||||
</script>
|
61
src/components/panels/History/HistoryListRowCell.vue
Normal file
61
src/components/panels/History/HistoryListRowCell.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<td :class="cssClass">{{ output }}</td>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { HistoryListHeadertype } from '@/components/panels/HistoryListPanel.vue'
|
||||
import { formatFilesize, formatPrintTime } from '@/plugins/helpers'
|
||||
|
||||
@Component
|
||||
export default class HistoryListRowCell extends Mixins(BaseMixin) {
|
||||
@Prop({ type: Object, required: true }) job!: ServerHistoryStateJob
|
||||
@Prop({ type: Object, required: true }) col!: HistoryListHeadertype
|
||||
|
||||
get cssClass() {
|
||||
if (this.col.outputType === 'date') return ''
|
||||
|
||||
return 'text-no-wrap'
|
||||
}
|
||||
|
||||
get value() {
|
||||
if (this.col.value in this.job) return this.job[this.col.value]
|
||||
|
||||
const metadata = this.job.metadata ?? null
|
||||
if (metadata && this.col.value in metadata) return metadata[this.col.value]
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
get output() {
|
||||
// return fallback if value is null or 0
|
||||
if (this.value === null || this.value === 0) return '--'
|
||||
|
||||
// direct output of strings or other non-numeric values
|
||||
if (typeof this.value !== 'number') return this.value
|
||||
|
||||
switch (this.col.outputType) {
|
||||
case 'filesize':
|
||||
return formatFilesize(this.value)
|
||||
|
||||
case 'date':
|
||||
return this.formatDateTime(this.value * 1000)
|
||||
|
||||
case 'time':
|
||||
return formatPrintTime(this.value)
|
||||
|
||||
case 'temp':
|
||||
return this.value?.toFixed() + ' °C'
|
||||
|
||||
case 'length':
|
||||
if (this.value > 1000) return (this.value / 1000).toFixed(2) + ' m'
|
||||
|
||||
return this.value?.toFixed(2) + ' mm'
|
||||
|
||||
default:
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
File diff suppressed because it is too large
Load Diff
@ -12,19 +12,19 @@
|
||||
<template v-if="existsSelectedJobs">
|
||||
<tr>
|
||||
<td>{{ $t('History.SelectedPrinttime') }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(selectedPrintTime) }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(selectedPrintTime, false) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('History.LongestPrinttime') }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(selectedLongestPrintTime) }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(selectedLongestPrintTime, false) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('History.AvgPrinttime') }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(selectedAvgPrintTime) }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(selectedAvgPrintTime, false) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('History.SelectedFilamentUsed') }}</td>
|
||||
<td class="text-right">{{ Math.round(selectedFilamentUsed / 100) / 10 }} m</td>
|
||||
<td class="text-right">{{ selectedFilamentUsedFormat }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('History.SelectedJobs') }}</td>
|
||||
@ -34,19 +34,19 @@
|
||||
<template v-else>
|
||||
<tr>
|
||||
<td>{{ $t('History.TotalPrinttime') }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(totalPrintTime) }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(totalPrintTime, false) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('History.LongestPrinttime') }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(longestPrintTime) }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(longestPrintTime, false) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('History.AvgPrinttime') }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(avgPrintTime) }}</td>
|
||||
<td class="text-right">{{ formatPrintTime(avgPrintTime, false) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('History.TotalFilamentUsed') }}</td>
|
||||
<td class="text-right">{{ Math.round(totalFilamentUsed / 100) / 10 }} m</td>
|
||||
<td class="text-right">{{ totalFilamentUsedFormat }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('History.TotalJobs') }}</td>
|
||||
@ -57,17 +57,12 @@
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col class="col-12 col-sm-6 col-md-4">
|
||||
<history-all-print-status-chart
|
||||
v-if="togglePrintStatus === 'chart'"></history-all-print-status-chart>
|
||||
<history-all-print-status-table v-else></history-all-print-status-table>
|
||||
<history-all-print-status-chart v-if="togglePrintStatus === 'chart'" />
|
||||
<history-all-print-status-table v-else />
|
||||
<div class="text-center mb-3">
|
||||
<v-btn-toggle v-model="togglePrintStatus" small mandatory>
|
||||
<v-btn small value="chart">
|
||||
{{ $t('History.Chart') }}
|
||||
</v-btn>
|
||||
<v-btn small value="table">
|
||||
{{ $t('History.Table') }}
|
||||
</v-btn>
|
||||
<v-btn small value="chart">{{ $t('History.Chart') }}</v-btn>
|
||||
<v-btn small value="table">{{ $t('History.Table') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-tooltip v-if="!allLoaded" top>
|
||||
<template #activator="{ on, attrs }">
|
||||
@ -88,16 +83,12 @@
|
||||
</div>
|
||||
</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-else-if="toggleChart === 'printtime_avg'"></history-printtime-avg>
|
||||
<history-filament-usage v-if="toggleChart === 'filament_usage'" />
|
||||
<history-printtime-avg v-else-if="toggleChart === '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 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>
|
||||
@ -115,15 +106,17 @@ import HistoryPrinttimeAvg from '@/components/charts/HistoryPrinttimeAvg.vue'
|
||||
import HistoryAllPrintStatusChart from '@/components/charts/HistoryAllPrintStatusChart.vue'
|
||||
import { ServerHistoryStateJob } from '@/store/server/history/types'
|
||||
import { mdiChartAreaspline, mdiDatabaseArrowDownOutline } from '@mdi/js'
|
||||
import { formatPrintTime } from '@/plugins/helpers'
|
||||
@Component({
|
||||
components: { Panel, HistoryFilamentUsage, HistoryPrinttimeAvg, HistoryAllPrintStatusChart },
|
||||
})
|
||||
export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
|
||||
mdiChartAreaspline = mdiChartAreaspline
|
||||
mdiDatabaseArrowDownOutline = mdiDatabaseArrowDownOutline
|
||||
formatPrintTime = formatPrintTime
|
||||
|
||||
get selectedJobs() {
|
||||
return this.$store.state.gui.view.history.selectedJobs ?? []
|
||||
return this.$store.getters['server/history/getSelectedJobs']
|
||||
}
|
||||
|
||||
get existsSelectedJobs() {
|
||||
@ -131,9 +124,7 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
|
||||
}
|
||||
|
||||
get totalPrintTime() {
|
||||
return 'total_print_time' in this.$store.state.server.history.job_totals
|
||||
? this.$store.state.server.history.job_totals.total_print_time
|
||||
: 0
|
||||
return this.$store.state.server.history.job_totals?.total_print_time ?? 0
|
||||
}
|
||||
|
||||
get selectedPrintTime() {
|
||||
@ -147,9 +138,7 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
|
||||
}
|
||||
|
||||
get longestPrintTime() {
|
||||
return 'longest_print' in this.$store.state.server.history.job_totals
|
||||
? this.$store.state.server.history.job_totals.longest_print
|
||||
: 0
|
||||
return this.$store.state.server.history.job_totals?.longest_print ?? 0
|
||||
}
|
||||
|
||||
get selectedLongestPrintTime() {
|
||||
@ -177,9 +166,13 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
|
||||
}
|
||||
|
||||
get totalFilamentUsed() {
|
||||
return 'total_filament_used' in this.$store.state.server.history.job_totals
|
||||
? this.$store.state.server.history.job_totals.total_filament_used
|
||||
: 0
|
||||
return this.$store.state.server.history.job_totals?.total_filament_used ?? 0
|
||||
}
|
||||
|
||||
get totalFilamentUsedFormat() {
|
||||
const value = Math.round(this.totalFilamentUsed / 100) / 10
|
||||
|
||||
return `${value} m`
|
||||
}
|
||||
|
||||
get selectedFilamentUsed() {
|
||||
@ -192,10 +185,14 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
|
||||
return filamentUsed
|
||||
}
|
||||
|
||||
get selectedFilamentUsedFormat() {
|
||||
const value = Math.round(this.selectedFilamentUsed / 100) / 10
|
||||
|
||||
return `${value} m`
|
||||
}
|
||||
|
||||
get totalJobsCount() {
|
||||
return 'total_jobs' in this.$store.state.server.history.job_totals
|
||||
? this.$store.state.server.history.job_totals.total_jobs
|
||||
: 0
|
||||
return this.$store.state.server.history.job_totals?.total_jobs ?? 0
|
||||
}
|
||||
|
||||
get toggleChart() {
|
||||
@ -223,25 +220,5 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
|
||||
|
||||
this.$socket.emit('server.history.list', { start: 0, limit: 50 }, { action: 'server/history/getHistory' })
|
||||
}
|
||||
|
||||
formatPrintTime(totalSeconds: number) {
|
||||
if (totalSeconds) {
|
||||
let output = ''
|
||||
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
totalSeconds %= 3600
|
||||
if (hours) output += ' ' + hours + 'h'
|
||||
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
if (minutes) output += ' ' + minutes + 'm'
|
||||
|
||||
const seconds = totalSeconds % 60
|
||||
if (seconds) output += ' ' + seconds.toFixed(0) + 's'
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
return '--'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -16,6 +16,8 @@
|
||||
"KlipperRuntimeWarning": "Klipper runtime warning",
|
||||
"KlipperWarning": "Klipper warning"
|
||||
},
|
||||
"MaintenanceReminder": "Maintenance Reminder",
|
||||
"MaintenanceReminderText": "Maintenance \"{name}\" is due.",
|
||||
"MoonrakerWarnings": {
|
||||
"MoonrakerComponent": "Moonraker: {component}",
|
||||
"MoonrakerFailedComponentDescription": "An error was detected while loading the moonraker component '{component}'. Please check the log file and fix the issue.",
|
||||
@ -29,7 +31,11 @@
|
||||
"NextReboot": "next reboot",
|
||||
"NoNotification": "No Notification available",
|
||||
"Notifications": "Notifications",
|
||||
"Remind": "Remind:"
|
||||
"OneDayShort": "1D",
|
||||
"OneHourShort": "1H",
|
||||
"OneWeekShort": "1W",
|
||||
"Remind": "Remind:",
|
||||
"ShowDetails": "show details"
|
||||
},
|
||||
"NumberInput": {
|
||||
"GreaterOrEqualError": "Must be greater or equal than {min}!",
|
||||
@ -341,6 +347,8 @@
|
||||
"Wireframe": "Wireframe"
|
||||
},
|
||||
"History": {
|
||||
"AddANote": "Add a note",
|
||||
"AddMaintenance": "Add Maintenance",
|
||||
"AddNote": "Add note",
|
||||
"AddToQueueSuccessful": "File {filename} added to Queue.",
|
||||
"AllJobs": "All",
|
||||
@ -348,16 +356,26 @@
|
||||
"Cancel": "Cancel",
|
||||
"Chart": "Chart",
|
||||
"CreateNote": "Create Note",
|
||||
"DateBasedReminder": "Date",
|
||||
"DateBasedReminderDescription": "This reminder is based on the date.",
|
||||
"Days": "days",
|
||||
"Delete": "Delete",
|
||||
"DeleteSelectedQuestion": "Do you really want to delete {count} selected jobs?",
|
||||
"DeleteSingleJobQuestion": "Do you really want to delete the job?",
|
||||
"Details": "Details",
|
||||
"EditMaintenance": "Edit Maintenance",
|
||||
"EditNote": "Edit Note",
|
||||
"Empty": "empty",
|
||||
"EndTime": "End Time",
|
||||
"EntryCreatedAt": "Created at {date}.",
|
||||
"EntryNextPerform": "Next perform:",
|
||||
"EntryPerformedAt": "Performed at {date}.",
|
||||
"EntrySince": "Used since:",
|
||||
"EstimatedFilament": "Estimated Filament",
|
||||
"EstimatedFilamentWeight": "Estimated Filament Weight",
|
||||
"EstimatedTime": "Estimated Time",
|
||||
"FilamentBasedReminder": "Filament",
|
||||
"FilamentBasedReminderDescription": "This reminder is based on the filament usage.",
|
||||
"FilamentCalc": "Filament Calc",
|
||||
"FilamentUsage": "Filament usage",
|
||||
"FilamentUsed": "Filament Used",
|
||||
@ -368,18 +386,35 @@
|
||||
"FirstLayerHeight": "First Layer Height",
|
||||
"HistoryFilamentUsage": "Filament",
|
||||
"HistoryPrinttimeAVG": "Prints",
|
||||
"Hours": "hours",
|
||||
"InvalidNameEmpty": "Invalid name. Name must not be empty!",
|
||||
"JobDetails": "Job Details",
|
||||
"Jobs": "Jobs",
|
||||
"LastModified": "Last Modified",
|
||||
"LayerHeight": "Layer Height",
|
||||
"LoadCompleteHistory": "Load complete history",
|
||||
"LongestPrinttime": "Longest Print Time",
|
||||
"Maintenance": "Maintenance",
|
||||
"MaintenanceEntries": "Maintenance Entries",
|
||||
"Meter": "meter",
|
||||
"Name": "Name",
|
||||
"NoReminder": "No reminder",
|
||||
"Note": "Note",
|
||||
"ObjectHeight": "Object Height",
|
||||
"OneTime": "One-Time",
|
||||
"Perform": "perform",
|
||||
"Performed": "performed",
|
||||
"PerformedAndReschedule": "performed and reschedule",
|
||||
"PerformMaintenance": "Perform Maintenance",
|
||||
"PrintDuration": "Print Time",
|
||||
"PrintHistory": "Print History",
|
||||
"PrintJobs": "Print Jobs",
|
||||
"PrintTime": "Print Time",
|
||||
"PrinttimeAvg": "Print Time - Ø",
|
||||
"PrinttimeBasedReminder": "Print Time",
|
||||
"PrinttimeBasedReminderDescription": "This reminder is based on the print time.",
|
||||
"Reminder": "Reminder",
|
||||
"Repeat": "Repeat",
|
||||
"Reprint": "Reprint",
|
||||
"Save": "save",
|
||||
"Search": "search",
|
||||
@ -946,7 +981,8 @@
|
||||
"DbConsoleHistory": "Console History",
|
||||
"DbHistoryJobs": "History Jobs",
|
||||
"DbHistoryTotals": "History Totals",
|
||||
"DBNavigation": "Navigation",
|
||||
"DbMaintenance": "Maintenance",
|
||||
"DbNavigation": "Navigation",
|
||||
"DbTimelapseSettings": "Timelapse Settings",
|
||||
"DbView": "View Settings",
|
||||
"EstimateValues": {
|
||||
|
@ -17,11 +17,9 @@ import { Component, Mixins } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import HistoryListPanel from '@/components/panels/HistoryListPanel.vue'
|
||||
import HistoryStatisticsPanel from '@/components/panels/HistoryStatisticsPanel.vue'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
HistoryStatisticsPanel,
|
||||
HistoryListPanel,
|
||||
},
|
||||
components: { HistoryListPanel, HistoryStatisticsPanel },
|
||||
})
|
||||
export default class PageHistory extends Mixins(BaseMixin) {}
|
||||
</script>
|
||||
|
@ -112,30 +112,30 @@ export const formatFrequency = (frequency: number): string => {
|
||||
return Math.max(frequency, 0.1).toFixed() + units[i]
|
||||
}
|
||||
|
||||
export const formatPrintTime = (totalSeconds: number): string => {
|
||||
if (totalSeconds) {
|
||||
let output = ''
|
||||
export const formatPrintTime = (totalSeconds: number, boolDays = true): string => {
|
||||
if (!totalSeconds) return '--'
|
||||
|
||||
const output: string[] = []
|
||||
|
||||
if (boolDays) {
|
||||
const days = Math.floor(totalSeconds / (3600 * 24))
|
||||
if (days) {
|
||||
totalSeconds %= 3600 * 24
|
||||
output += days + 'd'
|
||||
output.push(`${days}d`)
|
||||
}
|
||||
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
totalSeconds %= 3600
|
||||
if (hours) output += ' ' + hours + 'h'
|
||||
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
if (minutes) output += ' ' + minutes + 'm'
|
||||
|
||||
const seconds = totalSeconds % 60
|
||||
if (seconds) output += ' ' + seconds.toFixed(0) + 's'
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
return '--'
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
totalSeconds %= 3600
|
||||
if (hours) output.push(`${hours}h`)
|
||||
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
if (minutes) output.push(`${minutes}m`)
|
||||
|
||||
const seconds = totalSeconds % 60
|
||||
if (seconds) output.push(`${seconds.toFixed(0)}s`)
|
||||
|
||||
return output.join(' ')
|
||||
}
|
||||
|
||||
export const sortFiles = (items: FileStateFile[] | null, sortBy: string[], sortDesc: boolean[]): FileStateFile[] => {
|
||||
|
@ -246,7 +246,7 @@ export const actions: ActionTree<GuiState, RootState> = {
|
||||
}
|
||||
|
||||
for (const key of payload) {
|
||||
if (['webcams', 'timelapse'].includes(key)) {
|
||||
if (['maintenance', 'timelapse', 'webcams'].includes(key)) {
|
||||
const url = baseUrl + '?namespace=' + key
|
||||
|
||||
const response = await fetch(url)
|
||||
|
@ -14,6 +14,7 @@ import { navigation } from '@/store/gui/navigation'
|
||||
import { notifications } from '@/store/gui/notifications'
|
||||
import { presets } from '@/store/gui/presets'
|
||||
import { remoteprinters } from '@/store/gui/remoteprinters'
|
||||
import { maintenance } from '@/store/gui/maintenance'
|
||||
import { webcams } from '@/store/gui/webcams'
|
||||
import { heightmap } from '@/store/gui/heightmap'
|
||||
|
||||
@ -243,6 +244,8 @@ export const getDefaultState = (): GuiState => {
|
||||
'object_height',
|
||||
],
|
||||
selectedJobs: [],
|
||||
showMaintenanceEntries: true,
|
||||
showPrintJobs: true,
|
||||
},
|
||||
jobqueue: {
|
||||
countPerPage: 10,
|
||||
@ -294,6 +297,7 @@ export const gui: Module<GuiState, any> = {
|
||||
console,
|
||||
gcodehistory,
|
||||
macros,
|
||||
maintenance,
|
||||
miscellaneous,
|
||||
navigation,
|
||||
notifications,
|
||||
|
172
src/store/gui/maintenance/actions.ts
Normal file
172
src/store/gui/maintenance/actions.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import Vue from 'vue'
|
||||
import { ActionTree } from 'vuex'
|
||||
import { GuiMaintenanceState, MaintenanceJson } from '@/store/gui/maintenance/types'
|
||||
import { RootState } from '@/store/types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { themeDir } from '@/store/variables'
|
||||
|
||||
export const actions: ActionTree<GuiMaintenanceState, RootState> = {
|
||||
reset({ commit }) {
|
||||
commit('reset')
|
||||
},
|
||||
|
||||
init() {
|
||||
Vue.$socket.emit(
|
||||
'server.database.get_item',
|
||||
{ namespace: 'maintenance' },
|
||||
{ action: 'gui/maintenance/initStore' }
|
||||
)
|
||||
},
|
||||
|
||||
async initDb({ dispatch, rootGetters }) {
|
||||
const baseUrl = rootGetters['socket/getUrl']
|
||||
const url = `${baseUrl}/server/files/config/${themeDir}/maintenance.json?time=${Date.now()}`
|
||||
|
||||
const defaults: MaintenanceJson = await fetch(url)
|
||||
.then((response) => {
|
||||
if (response.status !== 200) return { entries: [] }
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.catch((e) => {
|
||||
window.console.error('maintenance.json cannot be parsed', e)
|
||||
return { entries: [] }
|
||||
})
|
||||
|
||||
// stop, when no entries are available/found
|
||||
const entries = defaults.entries ?? []
|
||||
if (entries?.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const totals = await fetch(`${baseUrl}/server/history/totals`)
|
||||
.then((response) => {
|
||||
if (response.status !== 200) return {}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.then((response: any) => response.result?.job_totals ?? {})
|
||||
.catch((e) => {
|
||||
window.console.debug('History totals could not be loaded', e)
|
||||
})
|
||||
|
||||
const total_filament = totals.total_filament_used ?? 0
|
||||
const total_print_time = totals.total_print_time ?? 0
|
||||
const date = new Date().getTime() / 1000
|
||||
|
||||
entries.forEach((entry) => {
|
||||
dispatch('store', {
|
||||
entry: {
|
||||
name: entry.name,
|
||||
note: entry.note ?? '',
|
||||
start_time: date,
|
||||
end_time: null,
|
||||
start_filament: total_filament,
|
||||
end_filament: null,
|
||||
start_printtime: total_print_time,
|
||||
end_printtime: null,
|
||||
last_entry: null,
|
||||
|
||||
reminder: {
|
||||
type: entry.reminder?.type ?? null,
|
||||
|
||||
filament: {
|
||||
bool: entry.reminder?.filament?.bool ?? false,
|
||||
value: entry.reminder?.filament?.value ?? null,
|
||||
},
|
||||
|
||||
printtime: {
|
||||
bool: entry.reminder?.printtime?.bool ?? false,
|
||||
value: entry.reminder?.printtime?.value ?? null,
|
||||
},
|
||||
|
||||
date: {
|
||||
bool: entry.reminder?.date?.bool ?? false,
|
||||
value: entry.reminder?.date?.value ?? null,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async initStore({ commit, dispatch }, payload) {
|
||||
await commit('reset')
|
||||
await commit('initStore', payload)
|
||||
await dispatch('socket/removeInitModule', 'gui/maintenance/init', { root: true })
|
||||
},
|
||||
|
||||
upload(_, payload) {
|
||||
Vue.$socket.emit('server.database.post_item', {
|
||||
namespace: 'maintenance',
|
||||
key: payload.id,
|
||||
value: payload.value,
|
||||
})
|
||||
},
|
||||
|
||||
store({ commit, dispatch, state }, payload) {
|
||||
const id = uuidv4()
|
||||
|
||||
commit('store', { id, values: payload.entry })
|
||||
dispatch('upload', {
|
||||
id,
|
||||
value: state.entries[id],
|
||||
})
|
||||
},
|
||||
|
||||
update({ commit, dispatch }, payload) {
|
||||
const id = payload.id
|
||||
delete payload.id
|
||||
|
||||
commit('update', {
|
||||
id: id,
|
||||
entry: payload,
|
||||
})
|
||||
dispatch('upload', {
|
||||
id: id,
|
||||
value: payload,
|
||||
})
|
||||
},
|
||||
|
||||
delete({ commit }, payload) {
|
||||
commit('delete', payload)
|
||||
Vue.$socket.emit('server.database.delete_item', { namespace: 'maintenance', key: payload })
|
||||
},
|
||||
|
||||
perform({ dispatch, state, rootState }, payload: { id: string; note: string }) {
|
||||
const entry = state.entries[payload.id]
|
||||
if (!entry) return
|
||||
|
||||
const totalFilament = rootState.server?.history?.job_totals?.total_filament_used ?? 0
|
||||
const totalPrintTime = rootState.server?.history?.job_totals?.total_print_time ?? 0
|
||||
|
||||
entry.id = payload.id
|
||||
entry.end_time = Date.now() / 1000
|
||||
entry.end_filament = totalFilament
|
||||
entry.end_printtime = totalPrintTime
|
||||
entry.perform_note = payload.note.trim() || null
|
||||
|
||||
dispatch('update', entry)
|
||||
|
||||
if (entry.reminder.type === 'repeat') {
|
||||
const date = new Date()
|
||||
|
||||
dispatch('store', {
|
||||
entry: {
|
||||
name: entry.name,
|
||||
note: entry.note,
|
||||
// divided by 1000 to get seconds, because history entries are also in seconds
|
||||
start_time: date.getTime() / 1000,
|
||||
end_time: null,
|
||||
start_filament: totalFilament,
|
||||
end_filament: null,
|
||||
start_printtime: totalPrintTime,
|
||||
end_printtime: null,
|
||||
last_entry: payload.id,
|
||||
|
||||
reminder: { ...entry.reminder },
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
47
src/store/gui/maintenance/getters.ts
Normal file
47
src/store/gui/maintenance/getters.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { GetterTree } from 'vuex'
|
||||
import { GuiMaintenanceState, GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const getters: GetterTree<GuiMaintenanceState, any> = {
|
||||
getEntries: (state) => {
|
||||
const entries: GuiMaintenanceStateEntry[] = []
|
||||
|
||||
Object.keys(state.entries).forEach((id: string) => {
|
||||
entries.push({ ...state.entries[id], id })
|
||||
})
|
||||
|
||||
return entries
|
||||
},
|
||||
|
||||
getOverdueEntries: (state, getters, rootState) => {
|
||||
const currentTotalPrintTime = rootState.server.history.job_totals.total_print_time ?? 0
|
||||
const currentTotalFilamentUsed = rootState.server.history.job_totals.total_filament_used ?? 0
|
||||
const currentDate = new Date().getTime() / 1000
|
||||
|
||||
const entries: GuiMaintenanceStateEntry[] = getters['getEntries'] ?? []
|
||||
|
||||
return entries.filter((entry) => {
|
||||
if (entry.reminder.type === null || entry.end_time !== null) return false
|
||||
|
||||
if (entry.reminder.filament.bool) {
|
||||
const end = entry.start_filament + (entry.reminder.filament.value ?? 0)
|
||||
|
||||
if (end <= currentTotalFilamentUsed) return true
|
||||
}
|
||||
|
||||
if (entry.reminder.printtime.bool) {
|
||||
const end = entry.start_printtime + (entry.reminder.printtime.value ?? 0)
|
||||
|
||||
if (end <= currentTotalPrintTime) return true
|
||||
}
|
||||
|
||||
if (entry.reminder.date.bool) {
|
||||
const end = entry.start_time + (entry.reminder.date.value ?? 0) * 24 * 60 * 60
|
||||
|
||||
if (end <= currentDate) return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
},
|
||||
}
|
23
src/store/gui/maintenance/index.ts
Normal file
23
src/store/gui/maintenance/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Module } from 'vuex'
|
||||
import { GuiMaintenanceState } from '@/store/gui/maintenance/types'
|
||||
import { actions } from '@/store/gui/maintenance/actions'
|
||||
import { mutations } from '@/store/gui/maintenance/mutations'
|
||||
import { getters } from '@/store/gui/maintenance/getters'
|
||||
|
||||
export const getDefaultState = (): GuiMaintenanceState => {
|
||||
return {
|
||||
entries: {},
|
||||
}
|
||||
}
|
||||
|
||||
// initial state
|
||||
const state = getDefaultState()
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const maintenance: Module<GuiMaintenanceState, any> = {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
}
|
32
src/store/gui/maintenance/mutations.ts
Normal file
32
src/store/gui/maintenance/mutations.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import Vue from 'vue'
|
||||
import { MutationTree } from 'vuex'
|
||||
import { GuiMaintenanceState } from '@/store/gui/maintenance/types'
|
||||
import { getDefaultState } from './index'
|
||||
|
||||
export const mutations: MutationTree<GuiMaintenanceState> = {
|
||||
reset(state) {
|
||||
Object.assign(state, getDefaultState())
|
||||
},
|
||||
|
||||
initStore(state, payload) {
|
||||
Vue.set(state, 'entries', payload.value)
|
||||
},
|
||||
|
||||
store(state, payload) {
|
||||
Vue.set(state.entries, payload.id, payload.values)
|
||||
},
|
||||
|
||||
update(state, payload) {
|
||||
if (!(payload.id in state.entries)) return
|
||||
|
||||
const entry = { ...state.entries[payload.id] }
|
||||
Object.assign(entry, payload.entry)
|
||||
Vue.set(state.entries, payload.id, entry)
|
||||
},
|
||||
|
||||
delete(state, payload) {
|
||||
if (payload in state.entries) {
|
||||
Vue.delete(state.entries, payload)
|
||||
}
|
||||
},
|
||||
}
|
67
src/store/gui/maintenance/types.ts
Normal file
67
src/store/gui/maintenance/types.ts
Normal file
@ -0,0 +1,67 @@
|
||||
export interface GuiMaintenanceState {
|
||||
entries: {
|
||||
[key: string]: GuiMaintenanceStateEntry
|
||||
}
|
||||
}
|
||||
|
||||
export interface GuiMaintenanceStateEntry {
|
||||
id?: string
|
||||
name: string
|
||||
note: string
|
||||
perform_note: string | null
|
||||
start_time: number
|
||||
end_time: number | null
|
||||
start_filament: number
|
||||
end_filament: number | null
|
||||
start_printtime: number
|
||||
end_printtime: number | null
|
||||
last_entry: string | null
|
||||
|
||||
reminder: {
|
||||
type: null | 'one-time' | 'repeat'
|
||||
|
||||
filament: {
|
||||
bool: boolean
|
||||
value: number | null
|
||||
}
|
||||
|
||||
printtime: {
|
||||
bool: boolean
|
||||
value: number | null
|
||||
}
|
||||
|
||||
date: {
|
||||
bool: boolean
|
||||
value: number | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface HistoryListRowMaintenance extends GuiMaintenanceStateEntry {
|
||||
type: 'maintenance'
|
||||
select_id: string
|
||||
}
|
||||
|
||||
export interface MaintenanceJson {
|
||||
entries: MaintenanceJsonEntry[]
|
||||
}
|
||||
|
||||
interface MaintenanceJsonEntry {
|
||||
name: string
|
||||
note?: string
|
||||
reminder?: {
|
||||
type: null | 'one-time' | 'repeat'
|
||||
filament?: {
|
||||
bool: boolean
|
||||
value: number | null
|
||||
}
|
||||
printtime?: {
|
||||
bool: boolean
|
||||
value: number | null
|
||||
}
|
||||
date?: {
|
||||
bool: boolean
|
||||
value: number | null
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import { PrinterStateKlipperConfigWarning } from '@/store/printer/types'
|
||||
import { detect } from 'detect-browser'
|
||||
import semver from 'semver'
|
||||
import { minBrowserVersions } from '@/store/variables'
|
||||
import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
|
||||
|
||||
export const getters: GetterTree<GuiNotificationState, any> = {
|
||||
getNotifications: (state, getters) => {
|
||||
@ -34,6 +35,9 @@ export const getters: GetterTree<GuiNotificationState, any> = {
|
||||
// klipper warnings
|
||||
notifications = notifications.concat(getters['getNotificationsKlipperWarnings'])
|
||||
|
||||
// user-created reminders
|
||||
notifications = notifications.concat(getters['getNotificationsOverdueMaintenance'])
|
||||
|
||||
// browser warnings
|
||||
notifications = notifications.concat(getters['getNotificationsBrowserWarnings'])
|
||||
|
||||
@ -363,6 +367,37 @@ export const getters: GetterTree<GuiNotificationState, any> = {
|
||||
return notifications
|
||||
},
|
||||
|
||||
getNotificationsOverdueMaintenance: (state, getters, rootState, rootGetters) => {
|
||||
const notifications: GuiNotificationStateEntry[] = []
|
||||
let entries: GuiMaintenanceStateEntry[] = rootGetters['gui/maintenance/getOverdueEntries']
|
||||
if (entries.length == 0) return []
|
||||
|
||||
const date = rootState.server.system_boot_at ?? new Date()
|
||||
|
||||
// get all dismissed reminders and convert it to a string[]
|
||||
const remindersDismisses = rootGetters['gui/notifications/getDismissByCategory']('maintenance').map(
|
||||
(dismiss: GuiNotificationStateDismissEntry) => {
|
||||
return dismiss.id
|
||||
}
|
||||
)
|
||||
|
||||
// filter all dismissed reminders
|
||||
entries = entries.filter((entry) => !remindersDismisses.includes(entry.id))
|
||||
|
||||
entries.forEach((entry) => {
|
||||
notifications.push({
|
||||
id: `maintenance/${entry.id}`,
|
||||
priority: 'high',
|
||||
title: i18n.t('App.Notifications.MaintenanceReminder').toString(),
|
||||
description: i18n.t('App.Notifications.MaintenanceReminderText', { name: entry.name }).toString(),
|
||||
date,
|
||||
dismissed: false,
|
||||
})
|
||||
})
|
||||
|
||||
return notifications
|
||||
},
|
||||
|
||||
getDismiss: (state, getters, rootState) => {
|
||||
const currentTime = new Date()
|
||||
const systemBootAt = rootState.server.system_boot_at ?? new Date()
|
||||
|
60
src/store/gui/reminders/actions.ts
Normal file
60
src/store/gui/reminders/actions.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import Vue from 'vue'
|
||||
import { ActionTree } from 'vuex'
|
||||
import { GuiRemindersState } from '@/store/gui/reminders/types'
|
||||
import { RootState } from '@/store/types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export const actions: ActionTree<GuiRemindersState, RootState> = {
|
||||
reset({ commit }) {
|
||||
commit('reset')
|
||||
},
|
||||
|
||||
init() {
|
||||
Vue.$socket.emit('server.database.get_item', { namespace: 'reminders' }, { action: 'gui/reminders/initStore' })
|
||||
},
|
||||
|
||||
async initStore({ commit, dispatch }, payload) {
|
||||
await commit('reset')
|
||||
await commit('initStore', payload)
|
||||
await dispatch('socket/removeInitModule', 'gui/reminders/init', { root: true })
|
||||
},
|
||||
|
||||
upload(_, payload) {
|
||||
Vue.$socket.emit('server.database.post_item', { namespace: 'reminders', key: payload.id, value: payload.value })
|
||||
},
|
||||
|
||||
store({ commit, dispatch, state }, payload) {
|
||||
const id = uuidv4()
|
||||
|
||||
commit('store', { id, values: payload.values })
|
||||
dispatch('upload', {
|
||||
id,
|
||||
value: state.reminders[id],
|
||||
})
|
||||
},
|
||||
|
||||
update({ commit, dispatch, state }, payload) {
|
||||
commit('update', payload)
|
||||
dispatch('upload', {
|
||||
id: payload.id,
|
||||
value: state.reminders[payload.id],
|
||||
})
|
||||
},
|
||||
|
||||
delete({ commit }, payload) {
|
||||
commit('delete', payload)
|
||||
Vue.$socket.emit('server.database.delete_item', { namespace: 'reminders', key: payload })
|
||||
},
|
||||
|
||||
repeat({ dispatch, getters, state, rootState }, payload) {
|
||||
if (!(payload.id in state.reminders)) return
|
||||
const reminder = getters['getReminder'](payload.id)
|
||||
const new_start_time = rootState.server?.history?.job_totals.total_print_time || 0
|
||||
const snooze_epoch_time = Date.now()
|
||||
dispatch('update', {
|
||||
id: reminder.id,
|
||||
snooze_print_hours_timestamps: [...reminder.snooze_print_hours_timestamps, new_start_time],
|
||||
snooze_epoch_timestamps: [...reminder.snooze_epoch_timestamps, snooze_epoch_time],
|
||||
})
|
||||
},
|
||||
}
|
29
src/store/gui/reminders/getters.ts
Normal file
29
src/store/gui/reminders/getters.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { GetterTree } from 'vuex'
|
||||
import { GuiRemindersState, GuiRemindersStateReminder } from '@/store/gui/reminders/types'
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const getters: GetterTree<GuiRemindersState, any> = {
|
||||
getReminders: (state) => {
|
||||
const reminders: GuiRemindersStateReminder[] = []
|
||||
|
||||
Object.keys(state.reminders).forEach((id: string) => {
|
||||
reminders.push({ ...state.reminders[id], id })
|
||||
})
|
||||
|
||||
return reminders
|
||||
},
|
||||
|
||||
getReminder: (state, getters) => (id: string) => {
|
||||
const reminders = getters['getReminders'] ?? []
|
||||
|
||||
return reminders.find((reminder: GuiRemindersStateReminder) => reminder.id === id)
|
||||
},
|
||||
|
||||
getOverdueReminders: (state, getters, rootState) => {
|
||||
const currentTotalPrintTime = rootState.server.history.job_totals.total_print_time
|
||||
const reminders: GuiRemindersStateReminder[] = getters['getReminders'] ?? []
|
||||
return reminders.filter(
|
||||
(reminder) => reminder.time_delta - (currentTotalPrintTime - reminder.start_total_print_time) < 0
|
||||
)
|
||||
},
|
||||
}
|
23
src/store/gui/reminders/index.ts
Normal file
23
src/store/gui/reminders/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Module } from 'vuex'
|
||||
import { GuiRemindersState } from '@/store/gui/reminders/types'
|
||||
import { actions } from '@/store/gui/reminders/actions'
|
||||
import { mutations } from '@/store/gui/reminders/mutations'
|
||||
import { getters } from '@/store/gui/reminders/getters'
|
||||
|
||||
export const getDefaultState = (): GuiRemindersState => {
|
||||
return {
|
||||
reminders: {},
|
||||
}
|
||||
}
|
||||
|
||||
// initial state
|
||||
const state = getDefaultState()
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const reminders: Module<GuiRemindersState, any> = {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
}
|
32
src/store/gui/reminders/mutations.ts
Normal file
32
src/store/gui/reminders/mutations.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import Vue from 'vue'
|
||||
import { MutationTree } from 'vuex'
|
||||
import { GuiRemindersState } from '@/store/gui/reminders/types'
|
||||
import { getDefaultState } from './index'
|
||||
|
||||
export const mutations: MutationTree<GuiRemindersState> = {
|
||||
reset(state) {
|
||||
Object.assign(state, getDefaultState())
|
||||
},
|
||||
|
||||
initStore(state, payload) {
|
||||
Vue.set(state, 'reminders', payload.value)
|
||||
},
|
||||
|
||||
store(state, payload) {
|
||||
Vue.set(state.reminders, payload.id, payload.values)
|
||||
},
|
||||
|
||||
update(state, payload) {
|
||||
if (payload.id in state.reminders) {
|
||||
const reminder = { ...state.reminders[payload.id] }
|
||||
Object.assign(reminder, payload)
|
||||
Vue.set(state.reminders, payload.id, reminder)
|
||||
}
|
||||
},
|
||||
|
||||
delete(state, payload) {
|
||||
if (payload in state.reminders) {
|
||||
Vue.delete(state.reminders, payload)
|
||||
}
|
||||
},
|
||||
}
|
16
src/store/gui/reminders/types.ts
Normal file
16
src/store/gui/reminders/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface GuiRemindersState {
|
||||
reminders: {
|
||||
[key: string]: GuiRemindersStateReminder
|
||||
}
|
||||
}
|
||||
|
||||
export interface GuiRemindersStateReminder {
|
||||
id: string
|
||||
name: string
|
||||
start_total_print_time: number
|
||||
time_delta: number
|
||||
repeating: boolean
|
||||
snooze_print_hours_timestamps: number[]
|
||||
snooze_epoch_timestamps: number[]
|
||||
remaining_print_time?: number
|
||||
}
|
@ -168,6 +168,8 @@ export interface GuiState {
|
||||
hidePrintStatus: string[]
|
||||
hideColums: string[]
|
||||
selectedJobs: ServerHistoryStateJob[]
|
||||
showMaintenanceEntries: boolean
|
||||
showPrintJobs: boolean
|
||||
}
|
||||
jobqueue: {
|
||||
countPerPage: number
|
||||
|
@ -63,6 +63,10 @@ export const actions: ActionTree<ServerState, RootState> = {
|
||||
dispatch('socket/addInitModule', 'gui/webcam/init', { root: true })
|
||||
dispatch('gui/webcams/init', null, { root: true })
|
||||
}
|
||||
if (payload.namespaces?.includes('maintenance')) {
|
||||
dispatch('socket/addInitModule', 'gui/maintenance/init', { root: true })
|
||||
dispatch('gui/maintenance/init', null, { root: true })
|
||||
} else dispatch('gui/maintenance/initDb', null, { root: true })
|
||||
|
||||
commit('saveDbNamespaces', payload.namespaces)
|
||||
|
||||
|
@ -1,14 +1,25 @@
|
||||
import { GetterTree } from 'vuex'
|
||||
import {
|
||||
HistoryListRowJob,
|
||||
ServerHistoryState,
|
||||
ServerHistoryStateAllPrintStatusEntry,
|
||||
ServerHistoryStateJob,
|
||||
} from '@/store/server/history/types'
|
||||
import { mdiAlertOutline, mdiCheckboxMarkedCircleOutline, mdiCloseCircleOutline, mdiProgressClock } from '@mdi/js'
|
||||
import i18n from '@/plugins/i18n'
|
||||
import { HistoryListRowMaintenance } from '@/store/gui/maintenance/types'
|
||||
|
||||
// I don't know why I cannot import the type from the HistoryListPanel, that's why I have to define it here again
|
||||
type HistoryListPanelRow = HistoryListRowJob | HistoryListRowMaintenance
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const getters: GetterTree<ServerHistoryState, any> = {
|
||||
getSelectedJobs: (state, getters, rootState): ServerHistoryStateJob[] => {
|
||||
const entries: HistoryListPanelRow[] = rootState.gui.view.history.selectedJobs ?? []
|
||||
|
||||
return entries.filter((entry) => entry.type === 'job') as ServerHistoryStateJob[]
|
||||
},
|
||||
|
||||
getTotalPrintTime(state) {
|
||||
let output = 0
|
||||
|
||||
@ -150,11 +161,11 @@ export const getters: GetterTree<ServerHistoryState, any> = {
|
||||
getSelectedPrintStatusArray(state, getters, rootState) {
|
||||
const output: ServerHistoryStateAllPrintStatusEntry[] = []
|
||||
|
||||
rootState.gui.view.history.selectedJobs.forEach((current: ServerHistoryStateJob) => {
|
||||
getters.getSelectedJobs.forEach((current: ServerHistoryStateJob) => {
|
||||
const index = output.findIndex((element) => element.name === current.status)
|
||||
if (index !== -1) output[index].value += 1
|
||||
else {
|
||||
const displayName = i18n.te(`History.StatusValues.${current.status}`, 'en')
|
||||
const displayName = i18n.te(`History.StatusValues.${current.status}`, 'en').toString()
|
||||
? i18n.t(`History.StatusValues.${current.status}`).toString()
|
||||
: current.status
|
||||
const itemStyle = {
|
||||
@ -192,7 +203,7 @@ export const getters: GetterTree<ServerHistoryState, any> = {
|
||||
return output
|
||||
},
|
||||
|
||||
getFilamentUsageArray(state, getters, rootState) {
|
||||
getFilamentUsageArray(state, getters) {
|
||||
// eslint-disable-next-line
|
||||
const output: any = []
|
||||
const startDate = new Date()
|
||||
@ -202,9 +213,9 @@ export const getters: GetterTree<ServerHistoryState, any> = {
|
||||
let jobsFiltered = [
|
||||
...state.jobs.filter((job) => new Date(job.start_time * 1000) >= startDate && job.filament_used > 0),
|
||||
]
|
||||
if (rootState.gui.view.history.selectedJobs.length)
|
||||
if (getters.getSelectedJobs.length)
|
||||
jobsFiltered = [
|
||||
...rootState.gui.view.history.selectedJobs.filter(
|
||||
...getters.getSelectedJobs.filter(
|
||||
(job: ServerHistoryStateJob) =>
|
||||
new Date(job.start_time * 1000) >= startDate && job.filament_used > 0
|
||||
),
|
||||
@ -239,9 +250,9 @@ export const getters: GetterTree<ServerHistoryState, any> = {
|
||||
let jobsFiltered = [
|
||||
...state.jobs.filter((job) => new Date(job.start_time * 1000) >= startDate && job.status === 'completed'),
|
||||
]
|
||||
if (rootState.gui.view.history.selectedJobs.length)
|
||||
if (getters.getSelectedJobs.length)
|
||||
jobsFiltered = [
|
||||
...rootState.gui.view.history.selectedJobs.filter(
|
||||
...getters.getSelectedJobs.filter(
|
||||
(job: ServerHistoryStateJob) =>
|
||||
new Date(job.start_time * 1000) >= startDate && job.status === 'completed'
|
||||
),
|
||||
@ -292,7 +303,7 @@ export const getters: GetterTree<ServerHistoryState, any> = {
|
||||
|
||||
// find jobs via metadata
|
||||
const jobs = state.jobs.filter((job) => {
|
||||
return job.metadata?.size === filesize && Math.round(job.metadata?.modified * 1000) === modified
|
||||
return job.metadata?.size === filesize && Math.round((job.metadata?.modified ?? 0) * 1000) === modified
|
||||
})
|
||||
if (jobs.length) return jobs
|
||||
if (job_id) return jobs.filter((job) => job.job_id === job_id)
|
||||
@ -303,7 +314,7 @@ export const getters: GetterTree<ServerHistoryState, any> = {
|
||||
getPrintStatusByFilename: (state) => (filename: string, modified: number) => {
|
||||
if (state.jobs.length) {
|
||||
const job = state.jobs.find((job) => {
|
||||
return job.filename === filename && Math.round(job.metadata?.modified * 1000) === modified
|
||||
return job.filename === filename && Math.round((job.metadata?.modified ?? 0) * 1000) === modified
|
||||
})
|
||||
|
||||
return job?.status ?? ''
|
||||
@ -354,7 +365,7 @@ export const getters: GetterTree<ServerHistoryState, any> = {
|
||||
}
|
||||
},
|
||||
|
||||
getFilterdJobList: (state, getters, rootState) => {
|
||||
getFilteredJobList: (state, getters, rootState) => {
|
||||
const hideStatus = rootState.gui.view.history.hidePrintStatus
|
||||
|
||||
return state.jobs.filter((job: ServerHistoryStateJob) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { FileStateFileThumbnail } from '@/store/files/types'
|
||||
|
||||
export interface ServerHistoryState {
|
||||
jobs: ServerHistoryStateJob[]
|
||||
job_totals: {
|
||||
@ -18,7 +20,31 @@ export interface ServerHistoryStateJob {
|
||||
filament_used: number
|
||||
filename: string
|
||||
// eslint-disable-next-line
|
||||
metadata: any
|
||||
metadata: {
|
||||
print_start_time?: number
|
||||
job_id?: number
|
||||
size?: number
|
||||
slicer?: string
|
||||
slicer_version?: string
|
||||
layer_count?: number
|
||||
layer_height?: number
|
||||
first_layer_height?: number
|
||||
object_height?: number
|
||||
filament_total?: number
|
||||
filament_weight_total?: number
|
||||
estimated_time?: number
|
||||
thumbnails?: FileStateFileThumbnail[]
|
||||
first_layer_bed_temp?: number
|
||||
first_layer_extr_temp?: number
|
||||
gcode_start_byte?: number
|
||||
gcode_end_byte?: number
|
||||
filename?: string
|
||||
filesize?: number
|
||||
modified?: number
|
||||
uuid?: string
|
||||
nozzle_diameter?: number
|
||||
[key: string]: any
|
||||
}
|
||||
note?: string
|
||||
print_duration: number
|
||||
status: string
|
||||
@ -26,6 +52,11 @@ export interface ServerHistoryStateJob {
|
||||
total_duration: number
|
||||
}
|
||||
|
||||
export interface HistoryListRowJob extends ServerHistoryStateJob {
|
||||
type: 'job'
|
||||
select_id: string
|
||||
}
|
||||
|
||||
export interface ServerHistoryStateAllPrintStatusEntry {
|
||||
name: string
|
||||
displayName: string
|
||||
|
Loading…
x
Reference in New Issue
Block a user