feat: add option to hide other Klipper & Moonraker instances (#2029)

This commit is contained in:
Stefan Dej 2024-12-01 20:47:16 +01:00 committed by GitHub
parent a0f003c9eb
commit 34f3f08bd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 324 additions and 146 deletions

View File

@ -28,6 +28,10 @@
min-width: auto !important;
}
.minHeight30 {
min-height: 30px !important;
}
.minHeight36 {
min-height: 36px;
}

View File

@ -1,9 +1,3 @@
<style scoped>
.minheight30 {
min-height: 30px;
}
</style>
<template>
<div>
<v-menu v-model="showMenu" bottom left :offset-y="true" :close-on-content-click="false">
@ -18,7 +12,7 @@
{{ $t('App.TopCornerMenu.KlipperControl') }}
</v-subheader>
<v-list-item
class="minheight30 pr-2"
class="minHeight30 pr-2"
link
@click="checkDialog(klipperRestart, 'klipper', 'restart')">
<v-list-item-title>{{ $t('App.TopCornerMenu.KlipperRestart') }}</v-list-item-title>
@ -27,7 +21,7 @@
</v-list-item-action>
</v-list-item>
<v-list-item
class="minheight30 pr-2"
class="minHeight30 pr-2"
link
@click="checkDialog(klipperFirmwareRestart, 'klipper', 'firmwareRestart')">
<v-list-item-title>{{ $t('App.TopCornerMenu.KlipperFirmwareRestart') }}</v-list-item-title>
@ -37,42 +31,15 @@
</v-list-item>
</template>
<template v-if="services.length">
<v-divider v-if="klipperState !== 'disconnected'" class="mt-0"></v-divider>
<v-divider v-if="klipperState !== 'disconnected'" class="mt-0" />
<v-subheader class="pt-2" style="height: auto">
{{ $t('App.TopCornerMenu.ServiceControl') }}
</v-subheader>
<v-list-item v-for="service in services" :key="service" class="minheight30 pr-2">
<v-list-item-title>
<v-tooltip left>
<template #activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">
{{ service.charAt(0).toUpperCase() + service.slice(1) }}
</span>
</template>
<span>{{ getServiceState(service) }} ({{ getServiceSubState(service) }})</span>
</v-tooltip>
</v-list-item-title>
<v-list-item-action class="my-0 d-flex flex-row" style="min-width: auto">
<v-btn
v-if="getServiceState(service) === 'inactive'"
icon
small
@click="checkDialog(serviceStart, service, 'start')">
<v-icon small>{{ mdiPlay }}</v-icon>
</v-btn>
<v-btn v-else icon small @click="checkDialog(serviceRestart, service, 'restart')">
<v-icon small>{{ mdiRestart }}</v-icon>
</v-btn>
<v-btn
icon
small
:disabled="getServiceState(service) === 'inactive' || service === 'moonraker'"
:style="service === 'moonraker' ? 'visibility: hidden;' : ''"
@click="checkDialog(serviceStop, service, 'stop')">
<v-icon small>{{ mdiStop }}</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<top-corner-menu-service
v-for="service in services"
:key="service"
:service="service"
@close-menu="showMenu = false" />
</template>
<template v-if="powerDevices.length">
<v-divider class="mt-0"></v-divider>
@ -82,7 +49,7 @@
<v-list-item
v-for="(device, index) in powerDevices"
:key="index"
class="minheight30 pr-2"
class="minHeight30 pr-2"
:disabled="
device.status === 'error' ||
(device.locked_while_printing && ['printing', 'paused'].includes(printer_state))
@ -98,13 +65,13 @@
</template>
<v-divider class="mt-0"></v-divider>
<v-subheader class="pt-2" style="height: auto">{{ $t('App.TopCornerMenu.HostControl') }}</v-subheader>
<v-list-item class="minheight30 pr-2" link @click="checkDialog(hostReboot, 'host', 'reboot')">
<v-list-item class="minHeight30 pr-2" link @click="checkDialog(hostReboot, 'host', 'reboot')">
<v-list-item-title>{{ $t('App.TopCornerMenu.Reboot') }}</v-list-item-title>
<v-list-item-action class="my-0 d-flex flex-row" style="min-width: auto">
<v-icon class="mr-2" small>{{ mdiPower }}</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item class="minheight30 pr-2" link @click="checkDialog(hostShutdown, 'host', 'shutdown')">
<v-list-item class="minHeight30 pr-2" link @click="checkDialog(hostShutdown, 'host', 'shutdown')">
<v-list-item-title>{{ $t('App.TopCornerMenu.Shutdown') }}</v-list-item-title>
<v-list-item-action class="my-0 d-flex flex-row" style="min-width: auto">
<v-icon class="mr-2" small>{{ mdiPower }}</v-icon>
@ -133,35 +100,14 @@
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogConfirmation.show" width="400" :fullscreen="isMobile">
<panel
card-class="confirm-top-corner-menu-dialog"
:icon="mdiAlert"
:title="dialogConfirmation.title"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="dialogConfirmation.show = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text class="pt-3">
<v-row>
<v-col>
<p class="body-2">{{ dialogConfirmation.description }}</p>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="dialogConfirmation.show = false">
{{ $t('App.TopCornerMenu.Cancel') }}
</v-btn>
<v-btn text color="error" @click="executeDialog">
{{ dialogConfirmation.actionButtonText }}
</v-btn>
</v-card-actions>
</panel>
</v-dialog>
<confirmation-dialog
:show="dialogConfirmation.show"
:title="dialogConfirmation.title"
:text="dialogConfirmation.description"
:action-button-text="dialogConfirmation.actionButtonText"
:cancel-button-text="$t('App.TopCornerMenu.Cancel')"
@action="executeDialog"
@close="dialogConfirmation.show = false" />
</div>
</template>
@ -171,17 +117,10 @@ import { Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { ServerPowerStateDevice } from '@/store/server/power/types'
import Panel from '@/components/ui/Panel.vue'
import {
mdiAlert,
mdiCloseThick,
mdiPowerStandby,
mdiRestart,
mdiPlay,
mdiPower,
mdiStop,
mdiToggleSwitch,
mdiToggleSwitchOff,
} from '@mdi/js'
import { mdiCloseThick, mdiPowerStandby, mdiRestart, mdiPower, mdiToggleSwitch, mdiToggleSwitchOff } from '@mdi/js'
import TopCornerMenuService from '@/components/ui/TopCornerMenuService.vue'
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
import ServiceMixins from '@/components/mixins/services'
interface dialogPowerDeviceChange {
show: boolean
@ -199,16 +138,13 @@ interface dialogConfirmation {
}
@Component({
components: { Panel },
components: { ConfirmationDialog, TopCornerMenuService, Panel },
})
export default class TheTopCornerMenu extends Mixins(BaseMixin) {
mdiAlert = mdiAlert
export default class TheTopCornerMenu extends Mixins(BaseMixin, ServiceMixins) {
mdiCloseThick = mdiCloseThick
mdiPowerStandby = mdiPowerStandby
mdiRestart = mdiRestart
mdiPlay = mdiPlay
mdiPower = mdiPower
mdiStop = mdiStop
mdiToggleSwitch = mdiToggleSwitch
mdiToggleSwitchOff = mdiToggleSwitchOff
@ -229,12 +165,28 @@ export default class TheTopCornerMenu extends Mixins(BaseMixin) {
}
get services() {
const services =
let services =
this.$store.state.server.system_info?.available_services?.filter(
(name: string) => name !== 'klipper_mcu'
) ?? []
services.sort()
return services
if (this.hideOtherInstances && this.klipperInstance !== '') {
services = services.filter(
(name: string) =>
(!name.toLowerCase().startsWith('klipper-') && name.toLowerCase() !== 'klipper') ||
name === this.klipperInstance
)
}
if (this.hideOtherInstances && this.moonrakerInstance !== '') {
services = services.filter(
(name: string) =>
(!name.toLowerCase().startsWith('moonraker-') && name.toLowerCase() !== 'moonraker') ||
name === this.moonrakerInstance
)
}
return services.sort()
}
get powerDevices() {
@ -243,50 +195,37 @@ export default class TheTopCornerMenu extends Mixins(BaseMixin) {
return devices.filter((device: ServerPowerStateDevice) => !device.device.startsWith('_'))
}
get service_states() {
return this.$store.state.server.system_info?.service_state ?? {}
}
getServiceState(name: string) {
if (name in this.service_states) return this.service_states[name].active_state
return null
}
getServiceSubState(name: string) {
if (name in this.service_states) return this.service_states[name].sub_state
return null
}
checkDialog(executableFunction: any, serviceName: string, action: string) {
if (this.printerIsPrinting) {
this.dialogConfirmation.executableFunction = executableFunction
this.dialogConfirmation.serviceName = serviceName
if (!this.printerIsPrinting) {
executableFunction(serviceName)
return
}
const actionUppercase = action.trim().charAt(0).toUpperCase() + action.trim().slice(1)
let titleKey = 'App.TopCornerMenu.ConfirmationDialog.Title.Service' + actionUppercase
let descriptionKey = 'App.TopCornerMenu.ConfirmationDialog.Description.Service' + actionUppercase
let buttonKey = 'App.TopCornerMenu.' + actionUppercase
this.dialogConfirmation.executableFunction = executableFunction
this.dialogConfirmation.serviceName = serviceName
if (serviceName === 'klipper' && ['stop', 'restart', 'firmwareRestart'].includes(action)) {
titleKey =
'App.TopCornerMenu.ConfirmationDialog.Title.' +
(action !== 'stop' ? 'Klipper' : 'Service') +
actionUppercase
descriptionKey = 'App.TopCornerMenu.ConfirmationDialog.Description.Klipper' + actionUppercase
const actionUppercase = action.trim().charAt(0).toUpperCase() + action.trim().slice(1)
let titleKey = 'App.TopCornerMenu.ConfirmationDialog.Title.Service' + actionUppercase
let descriptionKey = 'App.TopCornerMenu.ConfirmationDialog.Description.Service' + actionUppercase
let buttonKey = 'App.TopCornerMenu.' + actionUppercase
if (action === 'firmwareRestart') buttonKey = 'App.TopCornerMenu.KlipperFirmwareRestart'
} else if (serviceName === 'host') {
titleKey = 'App.TopCornerMenu.ConfirmationDialog.Title.Host' + actionUppercase
descriptionKey = 'App.TopCornerMenu.ConfirmationDialog.Description.Host' + actionUppercase
}
if (serviceName === 'klipper' && ['stop', 'restart', 'firmwareRestart'].includes(action)) {
titleKey =
'App.TopCornerMenu.ConfirmationDialog.Title.' +
(action !== 'stop' ? 'Klipper' : 'Service') +
actionUppercase
descriptionKey = 'App.TopCornerMenu.ConfirmationDialog.Description.Klipper' + actionUppercase
this.dialogConfirmation.title = this.$t(titleKey).toString()
this.dialogConfirmation.description = this.$t(descriptionKey).toString()
this.dialogConfirmation.actionButtonText = this.$t(buttonKey).toString()
this.dialogConfirmation.show = true
} else executableFunction(serviceName)
if (action === 'firmwareRestart') buttonKey = 'App.TopCornerMenu.KlipperFirmwareRestart'
} else if (serviceName === 'host') {
titleKey = 'App.TopCornerMenu.ConfirmationDialog.Title.Host' + actionUppercase
descriptionKey = 'App.TopCornerMenu.ConfirmationDialog.Description.Host' + actionUppercase
}
this.dialogConfirmation.title = this.$t(titleKey).toString()
this.dialogConfirmation.description = this.$t(descriptionKey).toString()
this.dialogConfirmation.actionButtonText = this.$t(buttonKey).toString()
this.dialogConfirmation.show = true
}
executeDialog() {
@ -306,21 +245,6 @@ export default class TheTopCornerMenu extends Mixins(BaseMixin) {
this.$socket.emit('printer.gcode.script', { script: 'FIRMWARE_RESTART' })
}
serviceStart(service: string) {
this.showMenu = false
this.$socket.emit('machine.services.start', { service: service })
}
serviceRestart(service: string) {
this.showMenu = false
this.$socket.emit('machine.services.restart', { service: service })
}
serviceStop(service: string) {
this.showMenu = false
this.$socket.emit('machine.services.stop', { service: service })
}
changeSwitch(device: ServerPowerStateDevice, value: string) {
this.dialogPowerDeviceChange.device = device.device
this.dialogPowerDeviceChange.value = value

View File

@ -0,0 +1,52 @@
<template>
<v-dialog :value="show" width="400" :fullscreen="isMobile">
<panel card-class="confirm-top-corner-menu-dialog" :icon="mdiAlert" :title="title" :margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="close">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text class="pt-3">
<v-row>
<v-col>
<p class="body-2">{{ text }}</p>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="close">{{ cancelButtonText }}</v-btn>
<v-btn text color="error" @click="action">{{ actionButtonText }}</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</template>
<script lang="ts">
import Component from 'vue-class-component'
import Panel from '@/components/ui/Panel.vue'
import { Mixins, Prop } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiAlert, mdiCloseThick } from '@mdi/js'
@Component({
components: { Panel },
})
export default class ConfirmationDialog extends Mixins(BaseMixin) {
mdiAlert = mdiAlert
mdiCloseThick = mdiCloseThick
@Prop({ type: Boolean, required: true }) show!: boolean
@Prop({ type: String, required: true }) title!: string
@Prop({ type: String, required: true }) text!: string
@Prop({ type: String, required: true }) actionButtonText!: string
@Prop({ type: String, required: true }) cancelButtonText!: string
action() {
this.$emit('action')
}
close() {
this.$emit('close')
}
}
</script>

View File

@ -0,0 +1,21 @@
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export default class ServiceMixins extends Vue {
get hideOtherInstances() {
return this.$store.state.gui.uiSettings.hideOtherInstances ?? false
}
get instance_ids() {
return this.$store.state.server.system_info?.instance_ids ?? {}
}
get klipperInstance() {
return this.instance_ids.klipper ?? ''
}
get moonrakerInstance() {
return this.instance_ids.moonraker ?? ''
}
}

View File

@ -322,6 +322,13 @@
$t('Settings.UiSettingsTab.DashboardHistoryLimitLabel', { count: dashboardHistoryLimit })
" />
</settings-row>
<v-divider class="my-2" />
<settings-row
:title="$t('Settings.UiSettingsTab.HideOtherInstances')"
:sub-title="$t('Settings.UiSettingsTab.HideOtherInstancesDescription')"
:dynamic-slot-width="true">
<v-switch v-model="hideOtherInstances" hide-details class="mt-0" />
</settings-row>
</v-card-text>
</v-card>
</div>
@ -696,6 +703,14 @@ export default class SettingsUiSettingsTab extends Mixins(BaseMixin, ThemeMixin)
this.$store.dispatch('gui/saveSetting', { name: 'uiSettings.dashboardHistoryLimit', value: newVal })
}
get hideOtherInstances() {
return this.$store.state.gui.uiSettings.hideOtherInstances ?? false
}
set hideOtherInstances(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'uiSettings.hideOtherInstances', value: newVal })
}
clearColorObject(color: any): string {
if (typeof color === 'object' && 'hex' in color) color = color.hex
if (color.length > 7) color = color.substr(0, 7)

View File

@ -0,0 +1,154 @@
<template>
<v-list-item class="minHeight30 pr-2">
<v-list-item-title>
<v-tooltip left>
<template #activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">{{ name }}</span>
</template>
<span>{{ state }} ({{ subState }})</span>
</v-tooltip>
</v-list-item-title>
<v-list-item-action class="my-0 d-flex flex-row" style="min-width: auto">
<v-btn v-if="state === 'inactive'" icon small @click="clickStart">
<v-icon small>{{ mdiPlay }}</v-icon>
</v-btn>
<v-btn v-else icon small @click="clickRestart">
<v-icon small>{{ mdiRestart }}</v-icon>
</v-btn>
<v-btn icon small :disabled="disableStopButton" :style="styleStopButton" @click="clickStop">
<v-icon small>{{ mdiStop }}</v-icon>
</v-btn>
</v-list-item-action>
<confirmation-dialog
:show="showRestartDialog"
:title="dialogRestartTitle"
:text="dialogRestartDescription"
:action-button-text="$t('App.TopCornerMenu.Restart')"
:cancel-button-text="$t('App.TopCornerMenu.Cancel')"
@action="serviceRestart"
@close="showRestartDialog = false" />
<confirmation-dialog
:show="showStopDialog"
:title="dialogStopTitle"
:text="dialogStopDescription"
:action-button-text="$t('App.TopCornerMenu.Stop')"
:cancel-button-text="$t('App.TopCornerMenu.Cancel')"
@action="serviceStop"
@close="showStopDialog = false" />
</v-list-item>
</template>
<script lang="ts">
import Component from 'vue-class-component'
import { Mixins, Prop } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiPlay, mdiRestart, mdiStop } from '@mdi/js'
import { capitalize } from '@/plugins/helpers'
import ServiceMixins from '@/components/mixins/services'
@Component({})
export default class TopCornerMenuService extends Mixins(BaseMixin, ServiceMixins) {
mdiPlay = mdiPlay
mdiRestart = mdiRestart
mdiStop = mdiStop
@Prop({ type: String, required: true }) service!: string
showRestartDialog = false
showStopDialog = false
get name() {
if (this.hideOtherInstances && this.service === this.klipperInstance) return 'Klipper'
if (this.hideOtherInstances && this.service === this.moonrakerInstance) return 'Moonraker'
return capitalize(this.service)
}
get service_states() {
return this.$store.state.server.system_info?.service_state ?? {}
}
get state() {
if (this.service in this.service_states) return this.service_states[this.service].active_state
return null
}
get subState() {
if (this.service in this.service_states) return this.service_states[this.service].sub_state
return null
}
get dialogRestartTitle() {
if (this.service === this.klipperInstance)
return this.$t('App.TopCornerMenu.ConfirmationDialog.Title.KlipperRestart')
return this.$t('App.TopCornerMenu.ConfirmationDialog.Title.ServiceRestart')
}
get dialogStopTitle() {
return this.$t('App.TopCornerMenu.ConfirmationDialog.Title.ServiceStop')
}
get dialogRestartDescription() {
if (this.service === this.klipperInstance)
return this.$t('App.TopCornerMenu.ConfirmationDialog.Description.KlipperRestart')
return this.$t('App.TopCornerMenu.ConfirmationDialog.Description.ServiceRestart')
}
get dialogStopDescription() {
if (this.service === this.klipperInstance)
return this.$t('App.TopCornerMenu.ConfirmationDialog.Description.KlipperStop')
return this.$t('App.TopCornerMenu.ConfirmationDialog.Description.ServiceStop')
}
get disableStopButton() {
return this.state === 'inactive' || this.service === this.moonrakerInstance
}
get styleStopButton() {
return this.service === this.moonrakerInstance ? 'visibility: hidden;' : ''
}
clickStart() {
this.$socket.emit('machine.services.start', { service: this.service })
this.closeMenu()
}
clickRestart() {
if (this.printerIsPrinting) {
this.showRestartDialog = true
return
}
this.serviceRestart()
}
clickStop() {
if (this.printerIsPrinting) {
this.showStopDialog = true
return
}
this.serviceStop()
}
serviceRestart() {
this.showRestartDialog = false
this.$socket.emit('machine.services.restart', { service: this.service })
this.closeMenu()
}
serviceStop() {
this.showStopDialog = false
this.$socket.emit('machine.services.stop', { service: this.service })
this.closeMenu()
}
closeMenu() {
this.$emit('close-menu')
}
}
</script>

View File

@ -1253,6 +1253,8 @@
"GcodeThumbnails": "G-Code thumbnails",
"GcodeThumbnailsDescription": "Click on the button to get to the instructions.",
"Guide": "Guide",
"HideOtherInstances": "Hide other instances",
"HideOtherInstancesDescription": "Hide other instances of Klipper & Moonraker in the Service Menu.",
"HideSaveConfigButtonForBedMesh": "Hide SAVE_CONFIG button for bed_mesh changes",
"HideSaveConfigButtonForBedMeshDescription": "Hide SAVE_CONFIG, if only bed_mesh changes are pending to be saved in Klipper.",
"HideUpdateWarnings": "Hide Update Warnings",

View File

@ -186,6 +186,7 @@ export const getDefaultState = (): GuiState => {
dashboardFilesLimit: 5,
dashboardFilesFilter: ['new', 'failed', 'completed'],
dashboardHistoryLimit: 5,
hideOtherInstances: false,
},
view: {
blockFileUpload: false,

View File

@ -128,6 +128,7 @@ export interface GuiState {
dashboardFilesLimit: number
dashboardFilesFilter: GuiStateUiSettingsDashboardFilesFilter[]
dashboardHistoryLimit: number
hideOtherInstances: boolean
}
view: {
blockFileUpload: boolean

View File

@ -33,6 +33,10 @@ export interface ServerState {
[key: string]: ServerStateNetwork
}
system_uptime: number | null
instance_ids: {
moonraker: string
klipper: string
}
} | null
system_boot_at: Date | null
moonraker_stats: {