From e16808b77ddca8acfc40502911ec0bd3ed632364 Mon Sep 17 00:00:00 2001 From: Mark Murnane Date: Wed, 16 Oct 2024 02:49:01 -0400 Subject: [PATCH] Adding progress indicators to actions --- backend/tuber/api/uber.py | 8 ++- backend/tuber/backgroundjobs.py | 19 ++++-- backend/tuber/config.py | 3 +- frontend/src/App.vue | 2 +- frontend/src/components/Progress.vue | 31 ++++++++++ .../rooming/requests/RequestTable.vue | 1 - frontend/src/lib/rest.ts | 62 ++++++++++++------- frontend/src/main.ts | 6 +- frontend/src/views/Actions.vue | 12 ++-- 9 files changed, 105 insertions(+), 39 deletions(-) create mode 100644 frontend/src/components/Progress.vue diff --git a/backend/tuber/api/uber.py b/backend/tuber/api/uber.py index e44d67b..a985900 100644 --- a/backend/tuber/api/uber.py +++ b/backend/tuber/api/uber.py @@ -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": { @@ -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 = [] @@ -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: diff --git a/backend/tuber/backgroundjobs.py b/backend/tuber/backgroundjobs.py index 1b37b78..c2f8b67 100644 --- a/backend/tuber/backgroundjobs.py +++ b/backend/tuber/backgroundjobs.py @@ -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")) @@ -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: @@ -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": @@ -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 diff --git a/backend/tuber/config.py b/backend/tuber/config.py index 68739f2..51b5c88 100644 --- a/backend/tuber/config.py +++ b/backend/tuber/config.py @@ -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": "{}" @@ -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]) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 0ad97d7..361a34b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -9,7 +9,7 @@
+ v-if="(loggedIn === false) && (initialSetup === false) && !($route.name === 'uberlogin') && !($route.name === 'uberdepartmentlogin')" />
diff --git a/frontend/src/components/Progress.vue b/frontend/src/components/Progress.vue new file mode 100644 index 0000000..768ed38 --- /dev/null +++ b/frontend/src/components/Progress.vue @@ -0,0 +1,31 @@ + + + diff --git a/frontend/src/components/rooming/requests/RequestTable.vue b/frontend/src/components/rooming/requests/RequestTable.vue index 27c3e57..d0bda6a 100644 --- a/frontend/src/components/rooming/requests/RequestTable.vue +++ b/frontend/src/components/rooming/requests/RequestTable.vue @@ -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 }) diff --git a/frontend/src/lib/rest.ts b/frontend/src/lib/rest.ts index 4b3aef7..b74a278 100644 --- a/frontend/src/lib/rest.ts +++ b/frontend/src/lib/rest.ts @@ -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, @@ -13,21 +18,23 @@ interface OptProgress { status?: string, messages?: string, active?: boolean, - definite?: boolean + definite?: boolean, + name?: string } async function wait (ms: number): Promise { 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) @@ -35,38 +42,47 @@ function setProgress (progress?: (n: Progress) => any, current?: OptProgress) { if (defaultProgress.amount) { defaultProgress.definite = true } - progress(defaultProgress) + progress.update(job, defaultProgress) } } -async function pollJob (response: Response, progressCB?: (n: Progress) => any): Promise { +async function pollJob (response: Response, jobid: String, progressTracker?: ProgressTracker, name?: string): Promise { 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 { +async function restFetch (method: string, url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise { 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', @@ -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 { - return await restFetch('GET', url, data, progressCB) +async function get (url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise { + return await restFetch('GET', url, data, progressTracker, name) } -async function post (url: string, data?: any, progressCB?: (n: Progress) => any): Promise { - return await restFetch('POST', url, data, progressCB) +async function post (url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise { + return await restFetch('POST', url, data, progressTracker, name) } -async function patch (url: string, data?: any, progressCB?: (n: Progress) => any): Promise { - return await restFetch('PATCH', url, data, progressCB) +async function patch (url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise { + return await restFetch('PATCH', url, data, progressTracker, name) } -async function del (url: string, data?: any, progressCB?: (n: Progress) => any): Promise { - return await restFetch('DELETE', url, data, progressCB) +async function del (url: string, data?: any, progressTracker?: ProgressTracker, name?: string): Promise { + return await restFetch('DELETE', url, data, progressTracker, name) } export { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 86f7ef9..1dab315 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -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) @@ -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) diff --git a/frontend/src/views/Actions.vue b/frontend/src/views/Actions.vue index 996b5d3..d63f4e1 100644 --- a/frontend/src/views/Actions.vue +++ b/frontend/src/views/Actions.vue @@ -5,6 +5,7 @@





+ @@ -15,9 +16,13 @@