Refactor upgrade websocket progress, so "anyone" can join in to see

now while an upgrade is executing, anyone with permission can "view"
the upgrade and see the same progress the executor is seeing
This commit is contained in:
Lance Edgar 2022-08-20 18:55:33 -05:00
parent 18cec49a86
commit e93063a344
3 changed files with 204 additions and 127 deletions

View file

@ -40,73 +40,22 @@
<%def name="extra_styles()"> <%def name="extra_styles()">
${parent.extra_styles()} ${parent.extra_styles()}
<style type="text/css"> % if master.has_perm('execute'):
.progress-with-textout { <style type="text/css">
border: 1px solid Black; .progress-with-textout {
line-height: 1.2; border: 1px solid Black;
overflow: auto; line-height: 1.2;
padding: 1rem; overflow: auto;
} padding: 1rem;
</style> }
</style>
% endif
</%def> </%def>
<%def name="render_this_page()"> <%def name="render_this_page()">
${parent.render_this_page()} ${parent.render_this_page()}
% if master.has_perm('execute'): % if expose_websockets and master.has_perm('execute'):
${h.form(master.get_action_url('declare_failure', instance), ref='declareFailureForm')}
${h.csrf_token(request)}
${h.end_form()}
% endif
</%def>
<%def name="render_buefy_form()">
<div class="form">
<${form.component}
% if master.has_perm('execute'):
@declare-failure="declareFailure"
% endif
>
</${form.component}>
</div>
</%def>
<%def name="render_form_buttons()">
% if not instance.executed and instance.status_code == enum.UPGRADE_STATUS_PENDING and master.has_perm('execute'):
<div class="buttons">
% if instance.enabled and not instance.executing:
% if use_buefy and expose_websockets:
<b-button type="is-primary"
icon-pack="fas"
icon-left="arrow-circle-right"
:disabled="upgradeExecuting"
@click="executeUpgrade()">
{{ upgradeExecuting ? "Working, please wait..." : "Execute this upgrade" }}
</b-button>
% elif use_buefy:
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), **{'@submit': 'submitForm'})}
${h.csrf_token(request)}
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="arrow-circle-right"
:disabled="formSubmitting">
{{ formSubmitting ? "Working, please wait..." : "Execute this upgrade" }}
</b-button>
${h.end_form()}
% else:
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), class_='autodisable')}
${h.csrf_token(request)}
${h.submit('execute', "Execute this upgrade", class_='button is-primary')}
${h.end_form()}
% endif
% elif instance.enabled:
<button type="button" class="button is-primary" disabled="disabled" title="This upgrade is currently executing">Execute this upgrade</button>
% else:
<button type="button" class="button is-primary" disabled="disabled" title="This upgrade is not enabled">Execute this upgrade</button>
% endif
</div>
<b-modal :active.sync="upgradeExecuting" <b-modal :active.sync="upgradeExecuting"
full-screen full-screen
:can-cancel="false"> :can-cancel="false">
@ -116,12 +65,15 @@
<div class="level"> <div class="level">
<div class="level-item has-text-centered" <div class="level-item has-text-centered"
style="display: flex; flex-direction: column;"> style="display: flex; flex-direction: column;">
<p class="block">Upgrading (please wait) ...</p> <p class="block">
Upgrading (please wait) ...
{{ executeUpgradeComplete ? "DONE!" : "" }}
</p>
<b-progress size="is-large" <b-progress size="is-large"
style="width: 400px;" style="width: 400px;"
## :value="80" ## :value="80"
## show-value ## show-value
## format="percent" ## format="percent"
> >
</b-progress> </b-progress>
</div> </div>
@ -151,7 +103,64 @@
</div> </div>
</div> </div>
</b-modal> </b-modal>
% endif
% if master.has_perm('execute'):
${h.form(master.get_action_url('declare_failure', instance), ref='declareFailureForm')}
${h.csrf_token(request)}
${h.end_form()}
% endif
</%def>
<%def name="render_buefy_form()">
<div class="form">
<${form.component}
% if expose_websockets and master.has_perm('execute'):
@execute-upgrade-click="executeUpgrade"
:upgrade-executing="upgradeExecuting"
@declare-failure-click="declareFailureClick"
:declare-failure-submitting="declareFailureSubmitting"
% endif
>
</${form.component}>
</div>
</%def>
<%def name="render_form_buttons()">
% if instance_executable and master.has_perm('execute'):
<div class="buttons">
% if instance.enabled and not instance.executing:
% if use_buefy and expose_websockets:
<b-button type="is-primary"
icon-pack="fas"
icon-left="arrow-circle-right"
:disabled="upgradeExecuting"
@click="$emit('execute-upgrade-click')">
{{ upgradeExecuting ? "Working, please wait..." : "Execute this upgrade" }}
</b-button>
% elif use_buefy:
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), **{'@submit': 'submitForm'})}
${h.csrf_token(request)}
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="arrow-circle-right"
:disabled="formSubmitting">
{{ formSubmitting ? "Working, please wait..." : "Execute this upgrade" }}
</b-button>
${h.end_form()}
% else:
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), class_='autodisable')}
${h.csrf_token(request)}
${h.submit('execute', "Execute this upgrade", class_='button is-primary')}
${h.end_form()}
% endif
% elif instance.enabled:
<button type="button" class="button is-primary" disabled="disabled" title="This upgrade is currently executing">Execute this upgrade</button>
% else:
<button type="button" class="button is-primary" disabled="disabled" title="This upgrade is not enabled">Execute this upgrade</button>
% endif
</div>
% endif % endif
</%def> </%def>
@ -165,69 +174,111 @@
% if expose_websockets: % if expose_websockets:
TailboneFormData.upgradeExecuting = ${json.dumps(instance.executing)|n} ThisPageData.ws = null
TailboneFormData.progressOutput = []
TailboneFormData.progressOutputCounter = 0
TailboneForm.methods.executeUpgrade = function() { //////////////////////////////
this.upgradeExecuting = true // execute upgrade
//////////////////////////////
TailboneForm.props.upgradeExecuting = {
type: Boolean,
default: false,
}
ThisPageData.upgradeExecuting = false
ThisPageData.progressOutput = []
ThisPageData.progressOutputCounter = 0
ThisPageData.executeUpgradeComplete = false
ThisPage.methods.adjustTextoutHeight = function() {
// grow the textout area to fill most of screen // grow the textout area to fill most of screen
let textout = this.$refs.textout
let height = window.innerHeight - textout.offsetTop - 50
textout.style.height = height + 'px'
}
ThisPage.methods.showExecuteDialog = function() {
this.upgradeExecuting = true
this.$nextTick(() => { this.$nextTick(() => {
let textout = this.$refs.textout this.adjustTextoutHeight()
let height = window.innerHeight - textout.offsetTop - 50
textout.style.height = height + 'px'
})
let url = '${master.get_action_url('execute', instance)}'
this.submitForm(url, {ws: true}, response => {
## TODO: should be a cleaner way to get this url?
url = '${request.route_url('ws.upgrades.execution_progress', _query={'uuid': instance.uuid})}'
url = url.replace(/^https?:/, 'wss:')
this.ws = new WebSocket(url)
let that = this
## TODO: add support for this here?
// this.ws.onclose = (event) => {
// // websocket closing means 1 of 2 things:
// // - user navigated away from page intentionally
// // - server connection was broken somehow
// // only one of those is "bad" and we only want to
// // display warning in 2nd case. so we simply use a
// // brief delay to "rule out" the 1st scenario
// setTimeout(() => { that.websocketBroken = true },
// 3000)
// }
this.ws.onmessage = (event) => {
let data = JSON.parse(event.data)
if (data.complete) {
// upgrade has completed; reload page to view result
location.reload()
} else if (data.stdout) {
// add lines to textout area
that.progressOutput.push({
key: ++that.progressOutputCounter,
text: data.stdout})
// scroll down to end of textout area
this.$nextTick(() => {
this.$refs.seeme.scrollIntoView()
})
}
}
}) })
} }
ThisPage.methods.establishWebsocket = function() {
## TODO: should be a cleaner way to get this url?
url = '${request.route_url('ws.upgrades.execution_progress', _query={'uuid': instance.uuid})}'
url = url.replace(/^https?:/, 'wss:')
this.ws = new WebSocket(url)
## TODO: add support for this here?
// this.ws.onclose = (event) => {
// // websocket closing means 1 of 2 things:
// // - user navigated away from page intentionally
// // - server connection was broken somehow
// // only one of those is "bad" and we only want to
// // display warning in 2nd case. so we simply use a
// // brief delay to "rule out" the 1st scenario
// setTimeout(() => { that.websocketBroken = true },
// 3000)
// }
this.ws.onmessage = (event) => {
let data = JSON.parse(event.data)
if (data.complete) {
// upgrade has completed; reload page to view result
this.executeUpgradeComplete = true
this.$nextTick(() => {
location.reload()
})
} else if (data.stdout) {
// add lines to textout area
this.progressOutput.push({
key: ++this.progressOutputCounter,
text: data.stdout})
// scroll down to end of textout area
this.$nextTick(() => {
this.$refs.seeme.scrollIntoView()
})
}
}
}
% if instance.executing:
ThisPage.mounted = function() {
this.showExecuteDialog()
this.establishWebsocket()
}
% endif
% if instance_executable:
ThisPage.methods.executeUpgrade = function() {
this.showExecuteDialog()
let url = '${master.get_action_url('execute', instance)}'
this.submitForm(url, {ws: true}, response => {
this.establishWebsocket()
})
}
% endif
% else: % else:
## no websockets ## no websockets
//////////////////////////////
// execute upgrade
//////////////////////////////
TailboneFormData.formSubmitting = false TailboneFormData.formSubmitting = false
TailboneForm.methods.submitForm = function() { TailboneForm.methods.submitForm = function() {
@ -236,17 +287,26 @@
% endif % endif
TailboneFormData.declareFailureSubmitting = false //////////////////////////////
// declare failure
//////////////////////////////
TailboneForm.methods.declareFailureClick = function() { TailboneForm.props.declareFailureSubmitting = {
if (confirm("Really declare this upgrade a failure?")) { type: Boolean,
this.declareFailureSubmitting = true default: false,
this.$emit('declare-failure')
}
} }
ThisPage.methods.declareFailure = function() { TailboneForm.methods.declareFailureClick = function() {
this.$refs.declareFailureForm.submit() this.$emit('declare-failure-click')
}
ThisPageData.declareFailureSubmitting = false
ThisPage.methods.declareFailureClick = function() {
if (confirm("Really declare this upgrade a failure?")) {
this.declareFailureSubmitting = true
this.$refs.declareFailureForm.submit()
}
} }
% endif % endif

View file

@ -1063,6 +1063,8 @@ class MasterView(View):
'instance_deletable': self.deletable_instance(instance), 'instance_deletable': self.deletable_instance(instance),
'form': form, 'form': form,
} }
if self.executable:
context['instance_executable'] = self.executable_instance(instance)
if hasattr(form, 'make_deform_form'): if hasattr(form, 'make_deform_form'):
context['dform'] = form.make_deform_form() context['dform'] = form.make_deform_form()
@ -1784,6 +1786,14 @@ class MasterView(View):
elif importer.allow_create: elif importer.allow_create:
return importer.create_object(key, host_data) return importer.create_object(key, host_data)
def executable_instance(self, instance):
"""
Returns boolean indicating whether or not the given instance
can be considered "executable". Returns ``True`` by default;
override as necessary.
"""
return True
def execute(self): def execute(self):
""" """
Execute an object. Execute an object.

View file

@ -475,6 +475,13 @@ class UpgradeView(MasterView):
# key = '{}.execute'.format(self.get_grid_key()) # key = '{}.execute'.format(self.get_grid_key())
# return SessionProgress(self.request, key, session_type='file') # return SessionProgress(self.request, key, session_type='file')
def executable_instance(self, upgrade):
if upgrade.executed:
return False
if upgrade.status_code != self.enum.UPGRADE_STATUS_PENDING:
return False
return True
def execute_instance(self, upgrade, user, progress=None, **kwargs): def execute_instance(self, upgrade, user, progress=None, **kwargs):
app = self.get_rattail_app() app = self.get_rattail_app()
session = app.get_session(upgrade) session = app.get_session(upgrade)