Skip to content

Commit

Permalink
Adding progress indicators to actions
Browse files Browse the repository at this point in the history
  • Loading branch information
bitbyt3r committed Oct 16, 2024
1 parent 1928a0d commit e16808b
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 39 deletions.
8 changes: 5 additions & 3 deletions backend/tuber/api/uber.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def import_shifts(event):
'X-Auth-Token': event_obj.uber_apikey
}
depts = db.query(Department).filter(Department.event == event).all()
for dept in depts:
for idx, dept in enumerate(depts):
g.progress(idx / len(depts), status=f"Importing shifts from {dept.name}")
req = {
"method": "shifts.lookup",
"params": {
Expand Down Expand Up @@ -118,7 +119,8 @@ def export_rooms(event):
hrr = {x.badge: x for x in hrr}
reqs = {}

for room in rooms:
for idx, room in enumerate(rooms):
g.progress(idx / len(rooms), status=f"Exporting Room {room.name}")
nights = []
assign = []
assigned = []
Expand Down Expand Up @@ -255,7 +257,7 @@ def sync_attendees(event):
else:
print(f"Skipping attendee {attendee} since I couldn't find it in Uber")
continue
if counter % 100 == 0:
if counter % 10 == 0:
g.progress(idx / len(eligible), status=f"Checking attendee {uber_model['full_name']}")
counter += 1
if attendee in badgelookup:
Expand Down
19 changes: 14 additions & 5 deletions backend/tuber/backgroundjobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ def __call__(self, environ, start_response):
start_response("404 Not Found", [])
return [bytes()]
if not progress['complete']:
start_response("202 Accepted", [("Content-Type", "application/json")])
headers = [("Content-Type", "application/json")]
if config.circuitbreaker_refresh:
headers.append(("Refresh", str(config.circuitbreaker_refresh)))
start_response("202 Accepted", headers)
return [json.dumps(progress).encode()]
data = r.get(f"{job_id}/data")
context = json.loads(r.get(f"{job_id}/context"))
Expand Down Expand Up @@ -117,7 +120,7 @@ def __call__(self, environ, start_response):
traceback.print_exc()
return ["Backend error".encode('UTF-8')]
request_context['state'] = "deferred"
progress = json.dumps({"complete": False, "amount": 0, "messages": "", "status": ""})
progress = environ.get("TUBER_JOB_PROGRESS", '{"complete": false, "amount": 0, "status": "", "messages": ""}')
if r:
r.set(f"{job_id}/progress", progress)
else:
Expand All @@ -138,6 +141,7 @@ def _write(_):
def _store_response(self, job_id, iterable):
if isinstance(iterable, Exception):
iterable = traceback.format_exception(None, iterable, iterable.__traceback__)
print("".join(iterable))
self.context[job_id]['status'] = "500 INTERNAL SERVER ERROR"
with self.lock:
if self.context[job_id]['state'] == "pending":
Expand Down Expand Up @@ -176,10 +180,15 @@ def _store_response(self, job_id, iterable):
db.commit()

def progress(self, amount, status=""):
job_id = request.environ.get("TUBER_JOB_ID", "")
if not job_id:
return
with self.lock:
job_id = request.environ.get("TUBER_JOB_ID", "")
if not job_id:
prior_progress = json.loads(request.environ.get("TUBER_JOB_PROGRESS", '{"complete": false, "amount": 0, "status": "", "messages": ""}'))
prior_progress['amount'] = amount
prior_progress['status'] = status
prior_progress['messages'] += status + "\n"
request.environ["TUBER_JOB_PROGRESS"] = json.dumps(prior_progress)
return
if r:
progress = json.loads(r.get(f"{job_id}/progress"))
progress['amount'] = amount
Expand Down
3 changes: 2 additions & 1 deletion backend/tuber/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"enable_circuitbreaker": False,
"circuitbreaker_timeout": 1,
"circuitbreaker_threads": 10,
"circuitbreaker_refresh": 1,
"redis_url": "",
"static_path": os.path.join(tuber.__path__[0], "static"),
"gender_map": "{}"
Expand All @@ -33,7 +34,7 @@
if isinstance(conf[i], str):
conf[i] = int(conf[i])

for i in ["circuitbreaker_timeout"]:
for i in ["circuitbreaker_timeout", "circuitbreaker_refresh"]:
if isinstance(conf[i], str):
conf[i] = float(conf[i])

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div class="layout-main">
<div class="card">
<login
v-if="(loggedIn === false) & (initialSetup === false) & !($route.name === 'uberlogin') & !($route.name === 'uberdepartmentlogin')" />
v-if="(loggedIn === false) && (initialSetup === false) && !($route.name === 'uberlogin') && !($route.name === 'uberdepartmentlogin')" />
<router-view v-else />
</div>
</div>
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/components/Progress.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div>
<Accordion v-if="Object.keys(jobs).length > 0">
<AccordionTab :header="job.name + ' - ' + job.status" v-for="job in jobs">
<ProgressBar :value="Math.round(job.amount * 100)" :mode="job.definite ? 'determinate' : 'indeterminate'"></ProgressBar>
<pre>{{ job.messages }}</pre>
</AccordionTab>
</Accordion>
</div>
</template>

<script>
export default {
name: 'Progress',
data () {
return {
jobs: {}
}
},
methods: {
update (job, progress) {
console.log("Updating job", job, progress)
this.jobs[job] = progress
},
stop_job (job) {
console.log("Stopping job", job)
delete this.jobs[job]
}
}
}
</script>
1 change: 0 additions & 1 deletion frontend/src/components/rooming/requests/RequestTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ export default {
},
async save (props) {
try {
console.log(props)
await patch('/api/event/' + this.event.id + '/hotel/request/' + this.editedID, props.edited)
props.cancel()
this.$toast.add({ severity: 'success', summary: 'Saved Successfully', detail: 'Your request has been saved. You may continue editing it until the deadline.', life: 3000 })
Expand Down
62 changes: 41 additions & 21 deletions frontend/src/lib/rest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { VueCookieNext } from 'vue-cookie-next'

interface ProgressTracker {
update: (job: String, progress: Progress) => null,
stop_job: (job: String) => null
}

interface Progress {
amount: number,
status: string,
Expand All @@ -13,60 +18,71 @@ interface OptProgress {
status?: string,
messages?: string,
active?: boolean,
definite?: boolean
definite?: boolean,
name?: string
}

async function wait (ms: number): Promise<null> {
return new Promise((resolve) => setTimeout(resolve, ms))
}

function setProgress (progress?: (n: Progress) => any, current?: OptProgress) {
function setProgress (job: String, progress?: ProgressTracker, current?: OptProgress) {
if (progress) {
const defaultProgress = {
amount: 0,
status: '',
messages: '',
active: true,
definite: false
definite: false,
name: ''
}
if (current) {
Object.assign(defaultProgress, current)
}
if (defaultProgress.amount) {
defaultProgress.definite = true
}
progress(defaultProgress)
progress.update(job, defaultProgress)
}
}

async function pollJob (response: Response, progressCB?: (n: Progress) => any): Promise<Response> {
async function pollJob (response: Response, jobid: String, progressTracker?: ProgressTracker, name?: string): Promise<Response> {
let delay = 100
const url = response.headers.get('location')
if (!url) {
throw new Error('Could not find job id.')
}
let currentAmount = 0
setProgress(progressCB)
setProgress(jobid, progressTracker, {name: name})
let job = await fetch(url)
while (job.status === 202) {
const progress = await job.json()
if (currentAmount === progress.amount) {
delay = delay * 1.5
}
let refresh = job.headers.get('Refresh')
if (refresh !== null) {
delay = parseFloat(refresh) * 1000
delay = Math.max(100, delay)
}
currentAmount = progress.amount
setProgress(progressCB, progress)
progress.name = name
setProgress(jobid, progressTracker, progress)
await wait(delay)
job = await fetch(url)
}
setProgress(progressCB, { active: false })
if (job.status > 202) {
setProgress(jobid, progressTracker, {amount: 1, status: "Failed ("+job.status+")", messages: await job.text(), name: name})
}
return job
}

async function restFetch (method: string, url: string, data?: any, progressCB?: (n: Progress) => any): Promise<any> {
async function restFetch (method: string, url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise<any> {
if (!data) {
data = {}
}
setProgress(progressCB, { active: true })
let jobid = new Array(5).join().replace(/(.|$)/g, function(){return ((Math.random()*36)|0).toString(36);})
setProgress(jobid, progressTracker, {name: name})

const headers: { [key: string]: string } = {
Accept: 'application/json',
Expand All @@ -88,31 +104,35 @@ async function restFetch (method: string, url: string, data?: any, progressCB?:
})

if (response.status === 200) {
setProgress(progressCB, { active: false })
if (progressTracker) {
progressTracker.stop_job(jobid)
}
return await response.json()
} else if (response.status === 202) {
const job = await pollJob(response, progressCB)
return await job.json()
const job = await pollJob(response, jobid, progressTracker, name)
let result = await job.json()
progressTracker?.stop_job(jobid)
return result
} else {
const msg = await response.text()
throw new Error(msg)
}
}

async function get (url: string, data?: any, progressCB?: (n: Progress) => any): Promise<any> {
return await restFetch('GET', url, data, progressCB)
async function get (url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise<any> {
return await restFetch('GET', url, data, progressTracker, name)
}

async function post (url: string, data?: any, progressCB?: (n: Progress) => any): Promise<any> {
return await restFetch('POST', url, data, progressCB)
async function post (url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise<any> {
return await restFetch('POST', url, data, progressTracker, name)
}

async function patch (url: string, data?: any, progressCB?: (n: Progress) => any): Promise<any> {
return await restFetch('PATCH', url, data, progressCB)
async function patch (url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise<any> {
return await restFetch('PATCH', url, data, progressTracker, name)
}

async function del (url: string, data?: any, progressCB?: (n: Progress) => any): Promise<any> {
return await restFetch('DELETE', url, data, progressCB)
async function del (url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise<any> {
return await restFetch('DELETE', url, data, progressTracker, name)
}

export {
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ app.directive('ripple', Ripple)
app.directive('badge', BadgeDirective)
app.directive('styleclass', StyleClass)

// app.component('Accordion', Accordion)
// app.component('AccordionTab', AccordionTab)
app.component('Accordion', Accordion)
app.component('AccordionTab', AccordionTab)
app.component('AutoComplete', AutoComplete)
// app.component('Avatar', Avatar)
// app.component('AvatarGroup', AvatarGroup)
Expand Down Expand Up @@ -181,7 +181,7 @@ app.component('Panel', Panel)
// app.component('PanelMenu', PanelMenu)
// app.component('Password', Password)
// app.component('PickList', PickList)
// app.component('ProgressBar', ProgressBar)
app.component('ProgressBar', ProgressBar)
// app.component('RadioButton', RadioButton)
// app.component('Rating', Rating)
// app.component('SelectButton', SelectButton)
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/views/Actions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<Button @click="import_shifts" label="Import Shifts"></Button><br /><br />
<Button @click="sync_attendees" label="Sync Attendees"></Button><br /><br />
<Button @click="export_rooms" label="Export Rooms to Uber"></Button><br /><br />
<Progress ref="progress" />
</div>
</template>

Expand All @@ -15,35 +16,38 @@
<script>
import { post } from '@/lib/rest'
import { mapGetters } from 'vuex'
import Progress from '../components/Progress.vue'
export default {
name: 'Actions',
components: {
Progress: Progress
},
computed: {
...mapGetters([
'event'
])
},
methods: {
async import_shifts () {
console.log(this.event);
try {
await post('/api/event/' + this.event.id + '/uber/import_shifts')
await post('/api/event/' + this.event.id + '/uber/import_shifts', null, this.$refs.progress, "Importing Shifts")
this.$toast.add({ severity: 'success', summary: 'Shifts Imported', life: 1000 })
} catch {
this.$toast.add({ severity: 'error', summary: 'Failed to import shifts', detail: 'Please contact your server administrator for assistance.', life: 3000 })
}
},
async sync_attendees () {
try {
await post('/api/event/' + this.event.id + '/uber/sync_attendees')
await post('/api/event/' + this.event.id + '/uber/sync_attendees', null, this.$refs.progress, "Syncing Attendees")
this.$toast.add({ severity: 'success', summary: 'Attendees Synced', life: 1000 })
} catch {
this.$toast.add({ severity: 'error', summary: 'Failed to sync attendees', detail: 'Please contact your server administrator for assistance.', life: 3000 })
}
},
async export_rooms () {
try {
await post('/api/event/' + this.event.id + '/uber/export_rooms')
await post('/api/event/' + this.event.id + '/uber/export_rooms', null, this.$refs.progress, "Exporting Rooms")
this.$toast.add({ severity: 'success', summary: 'Rooms Exported', life: 1000 })
} catch {
this.$toast.add({ severity: 'error', summary: 'Failed to export rooms', detail: 'Please contact your server administrator for assistance.', life: 3000 })
Expand Down

0 comments on commit e16808b

Please sign in to comment.