Skip to content

Commit 5330227

Browse files
[batch] Redesign batch front end pages (hail-is#14562)
This revamps the `batch.front_end` web pages to be a bit less cluttered and more usable. I brought in tailwindcss instead of using our existing sass. I mainly find it easier to work with but am fine porting it to sass if anyone feels strongly against changing css tools. In order to avoid overhauling all our HTML in one PR, I added a flag to the jinja context `use_tailwind` that is currently only on for the `batch` service. `layout.html` then uses this flag to determine which CSS and header it should use. If this gets to a state we're happy to merge, it should be a lot less work to bring over auth and CI and delete that flag.
1 parent 63db10a commit 5330227

18 files changed

+907
-785
lines changed

.dockerignore

+1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ hail/src/main/resources/build-info.properties
2929
hail/src/test/resources/example.v11.bgen.idx
3030
hail/src/test/resources/random.bgen.idx
3131
benchmark/python/build/
32+
web_common/web_common/static/css
3233
ci/repos

Makefile

+6-5
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ docs.tar.gz: hail/build/www
179179

180180
website-image: docs.tar.gz
181181

182-
$(SERVICES_IMAGES): %-image: $(SERVICES_IMAGE_DEPS) $(shell git ls-files $$* ':!:**/deployment.yaml')
182+
.SECONDEXPANSION:
183+
$(SERVICES_IMAGES): %-image: $(SERVICES_IMAGE_DEPS) $$(shell git ls-files $$* ':!:**/deployment.yaml')
183184
./docker-build.sh . $*/Dockerfile $(IMAGE_NAME) --build-arg BASE_IMAGE=$(shell cat hail-ubuntu-image)
184185
echo $(IMAGE_NAME) > $@
185186

@@ -246,17 +247,17 @@ $(SERVICES_DATABASES): %-db:
246247
python3 ci/create_local_database.py $* local-$*
247248
endif
248249

249-
.PHONY: sass-compile-watch
250-
sass-compile-watch:
251-
cd web_common/web_common && sass --watch -I styles --style=compressed styles:static/css
250+
.PHONY: tailwind-compile-watch
251+
tailwind-compile-watch:
252+
cd web_common && npx tailwindcss --watch -i input.css -o web_common/static/css/output.css
252253

253254
.PHONY: run-dev-proxy
254255
run-dev-proxy:
255256
SERVICE=$(SERVICE) adev runserver --root . devbin/dev_proxy.py
256257

257258
.PHONY: devserver
258259
devserver:
259-
$(MAKE) -j 2 sass-compile-watch run-dev-proxy
260+
$(MAKE) -j 2 tailwind-compile-watch run-dev-proxy
260261

261262
.PHONY: benchmark
262263
benchmark: hail-dev-image

batch/Dockerfile

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
ARG BASE_IMAGE={{ hail_ubuntu_image.image }}
22
FROM $BASE_IMAGE
33

4+
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.3/tailwindcss-linux-x64 && \
5+
chmod +x tailwindcss-linux-x64 && \
6+
mv tailwindcss-linux-x64 /usr/bin/tailwindcss
7+
48
COPY hail/python/hailtop/pinned-requirements.txt hailtop-requirements.txt
59
COPY gear/pinned-requirements.txt gear-requirements.txt
610
COPY web_common/pinned-requirements.txt web_common-requirements.txt
@@ -18,12 +22,16 @@ COPY hail/python/hailtop /hailtop/hailtop/
1822
COPY gear/setup.py /gear/setup.py
1923
COPY gear/gear /gear/gear/
2024

21-
COPY web_common/setup.py web_common/MANIFEST.in /web_common/
25+
COPY web_common/setup.py web_common/MANIFEST.in web_common/input.css web_common/tailwind.config.js /web_common/
2226
COPY web_common/web_common /web_common/web_common/
2327

2428
COPY batch/setup.py /batch/MANIFEST.in /batch/
2529
COPY batch/batch /batch/batch/
2630

31+
RUN cd web_common && \
32+
mkdir web_common/static/css && \
33+
tailwindcss -i input.css -o web_common/static/css/output.css
34+
2735
RUN hail-pip-install /hailtop /gear /web_common /batch
2836

2937
EXPOSE 5000
+117-110
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,131 @@
11
{% from "table_search.html" import table_search with context %}
2+
{% from "components/metadata_tables.html" import kv_table, resource_cost_table, collapsible_li %}
3+
{% from "components/utils.html" import
4+
batch_state_indicator, job_state_indicator, danger_button, submit_button, link
5+
%}
26
{% extends "layout.html" %}
37
{% block title %}Batch {{ batch['id'] }}{% endblock %}
48
{% block head %}
5-
<script src="{{ base_path }}/common_static/focus_on_keyup.js"></script>
9+
<script src="{{ base_path }}/common_static/focus_on_keyup.js"></script>
610
{% endblock %}
711
{% block content %}
12+
<div class='flex flex-wrap justify-around pt-8 gap-y-4'>
13+
<div class='drop-shadow-sm w-full md:basis-2/3 lg:basis-1/3'>
14+
<ul class='border border-collapse divide-y bg-slate-50 rounded'>
15+
<li class='p-4'>
16+
<div class='flex w-full justify-between items-center'>
17+
<div class='text-xl font-light'>Batch {{ batch['id'] }}</div>
18+
{{ batch_state_indicator(batch) }}
19+
</div>
20+
<div class='text-xl font-light py-2 overflow-auto'>
21+
{% if 'attributes' in batch and 'name' in batch['attributes'] %}
22+
{{ batch['attributes']['name'] }}
23+
{% endif %}
24+
</div>
25+
<div class='flex justify-between items-center'>
26+
<div>
27+
<div class='font-light text-zinc-500'>Submitted by {{ batch['user'] }}</div>
28+
<div class='font-light text-zinc-500'>Billed to {{ batch['billing_project'] }}</div>
29+
</div>
30+
{% if not batch['complete'] and batch['state'] != 'Cancelled' %}
31+
<form action="{{ base_path }}/batches/{{ batch['id'] }}/cancel" method="post">
32+
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
33+
{% if q is not none %}
34+
<input type="hidden" name="q" value="{{ q }}" />
35+
{% endif %}
36+
{{ danger_button('Cancel') }}
37+
</form>
38+
{% elif batch['complete'] %}
39+
<form action="{{ base_path }}/batches/{{ batch['id'] }}/delete" method="post">
40+
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
41+
{{ danger_button('Delete') }}
42+
</form>
43+
{% endif %}
44+
</div>
45+
</li>
846

9-
<h1>Batch {{ batch['id'] }}</h1>
10-
11-
<h2>Properties</h2>
12-
<ul>
13-
<li>User: {{ batch['user'] }}</li>
14-
<li>Billing Project: {{ batch['billing_project'] }}</li>
15-
<li>Time Created: {% if 'time_created' in batch and batch['time_created'] is not none %}{{ batch['time_created'] }}{% endif %}</li>
16-
<li>Time Completed: {% if 'time_completed' in batch and batch['time_completed'] is not none %}{{ batch['time_completed'] }}{% endif %}</li>
17-
<li>Total Jobs: {{ batch['n_jobs'] }}</li>
18-
<ul>
19-
<li>Pending Jobs: {{ batch['n_jobs'] - batch['n_completed'] }}</li>
20-
<li>Succeeded Jobs: {{ batch['n_succeeded'] }}</li>
21-
<li>Failed Jobs: {{ batch['n_failed'] }}</li>
22-
<li>Cancelled Jobs: {{ batch['n_cancelled'] }}</li>
23-
</ul>
24-
<li>Duration: {% if 'duration' in batch and batch['duration'] is not none %}{{ batch['duration'] }}{% endif %}</li>
25-
<li>Cost: {% if 'cost' in batch and batch['cost'] is not none %}{{ batch['cost'] }}{% endif %}</li>
26-
</ul>
27-
28-
{% if not batch['complete'] and batch['state'] != 'Cancelled' %}
29-
<form action="{{ base_path }}/batches/{{ batch['id'] }}/cancel" method="post">
30-
<input type="hidden" name="_csrf" value="{{ csrf_token }}"/>
31-
{% if q is not none %}
32-
<input type="hidden" name="q" value="{{ q }}"/>
33-
{% endif %}
34-
<button>Cancel</button>
35-
</form>
36-
{% endif %}
47+
{% call collapsible_li(true, 'Jobs', batch['n_jobs']) %}
48+
{{ kv_table({
49+
'Pending': batch['n_jobs'] - batch['n_completed'],
50+
'Succeeded': batch['n_succeeded'],
51+
'Failed': batch['n_failed'],
52+
'Cancelled': batch['n_cancelled']
53+
})}}
54+
{% endcall %}
3755

38-
<h2>Attributes</h2>
39-
{% if 'attributes' in batch %}
40-
{% for name, value in batch['attributes'].items() %}
41-
<p>{{ name }}: {{ value }}</p>
42-
{% endfor %}
43-
{% endif %}
56+
{% if 'attributes' in batch and batch['attributes'] %}
57+
{% call collapsible_li(false, 'Attributes', '') %}
58+
{{ kv_table(batch['attributes']) }}
59+
{% endcall %}
60+
{% endif %}
4461

45-
<h2>Cost Breakdown</h2>
46-
{% if batch['cost_breakdown'] %}
47-
<table class="data-table">
48-
<thead>
49-
<tr>
50-
<th>Resource</th>
51-
<th>Cost</th>
52-
</tr>
53-
</thead>
54-
<tbody>
55-
{% for resource_cost in batch['cost_breakdown'] %}
56-
<tr>
57-
<td>{{ resource_cost['resource'] }}</td>
58-
<td>{{ resource_cost['cost'] }}</td>
59-
</tr>
60-
{% endfor %}
61-
</tbody>
62-
</table>
63-
{% else %}
64-
<p>No accrued costs</p>
65-
{% endif %}
62+
{% call collapsible_li(false, 'Duration', batch.get('duration') or '') %}
63+
{{ kv_table({
64+
'Created': batch.get('time_created') or '',
65+
'Completed': batch.get('time_completed') or '',
66+
})}}
67+
{% endcall %}
6668

67-
<h2>Jobs</h2>
68-
<div class="flex-col">
69-
{{ table_search("job-search", base_path ~ "/batches/" ~ batch["id"]) }}
70-
<div class='flex-col' style="overflow: auto;">
71-
<table class="data-table" id="batch" style="width: 100%">
72-
<thead>
73-
<tr>
74-
<th>ID</th>
75-
<th>Name</th>
76-
<th>State</th>
77-
<th>Exit Code</th>
78-
<th>Duration</th>
79-
<th>Cost</th>
80-
</tr>
81-
</thead>
82-
<tbody>
83-
{% for job in batch['jobs'] %}
84-
<tr>
85-
<td class="numeric-cell">
86-
<a class="fill-td" href="{{ base_path }}/batches/{{ job['batch_id'] }}/jobs/{{ job['job_id'] }}">{{ job['job_id'] }}</a>
87-
</td>
88-
<td>
89-
{% if 'name' in job and job['name'] is not none %}
90-
{{ job['name'] }}
91-
{% endif %}
92-
</td>
93-
<td>{{ job['display_state'] }}</td>
94-
<td>
95-
{% if 'exit_code' in job and job['exit_code'] is not none %}
96-
{{ job['exit_code'] }}
97-
{% endif %}
98-
</td>
99-
<td>
100-
{% if 'duration' in job and job['duration'] is not none %}
101-
{{ job['duration'] }}
102-
{% endif %}
103-
</td>
104-
<td>
105-
{% if 'cost' in job and job['cost'] is not none %}
106-
{{ job['cost'] }}
107-
{% endif %}
108-
</td>
109-
</tr>
110-
{% endfor %}
111-
</tbody>
112-
</table>
69+
{% call collapsible_li(false, 'Cost', batch.get('cost')) %}
70+
{{ resource_cost_table(batch['cost_breakdown'] or {}) }}
71+
{% endcall %}
72+
</ul>
11373
</div>
114-
{% if last_job_id is not none %}
115-
<form method="GET" action="{{ base_path }}/batches/{{ batch['id'] }}">
116-
{% if q is not none %}
117-
<input type="hidden" name="q" value="{{ q }}" />
74+
<div class="flex flex-col w-full lg:basis-3/5">
75+
{{ table_search("job-search", base_path ~ "/batches/" ~ batch["id"]) }}
76+
<div class='flex flex-col mt-4'>
77+
<table class="table-fixed md:table-auto w-full" id="batch">
78+
<thead>
79+
<th class='h-16 bg-slate-200 font-light text-md text-left pl-4 rounded-tl w-1/12'>ID</th>
80+
<th class='h-16 bg-slate-200 font-light text-md text-left pl-4 rounded-tr md:rounded-tr-none w-3/4 lg:w-1/2'>
81+
Name</th>
82+
<th class='h-16 bg-slate-200 font-light text-md text-left pl-4 hidden lg:table-cell'>Duration</th>
83+
<th class='h-16 bg-slate-200 font-light text-md text-left pl-4 hidden md:table-cell rounded-tr'>Cost</th>
84+
</thead>
85+
<tbody class='border border-collapse border-slate-50'>
86+
{% for job in batch['jobs'] %}
87+
<tr class='border border-collapse hover:bg-slate-100'>
88+
<td class='font-light py-2 px-4'>
89+
{{ link(base_path ~ '/batches/' ~ job['batch_id'] ~ '/jobs/' ~ job['job_id'], job['job_id']) }}
90+
</td>
91+
<td class='py-2 px-4 block overflow-x-auto'>
92+
<div class='flex flex-col space-y-1 md:flex-row md:flex-wrap md:space-y-0'>
93+
{% if 'name' in job and job['name'] is not none %}
94+
<div class='text-wrap pr-4 font-normal text-lg'>
95+
{{ link(base_path ~ '/batches/' ~ job['batch_id'] ~ '/jobs/' ~ job['job_id'], job['name']) }}
96+
</div>
97+
{% else %}
98+
<div class='text-wrap pr-4 text-zinc-400 italic'>
99+
{{ link(base_path ~ '/batches/' ~ job['batch_id'] ~ '/jobs/' ~ job['job_id'], 'no name') }}
100+
</div>
101+
{% endif %}
102+
<div class='flex items-center'>
103+
{{ job_state_indicator(job) }}
104+
</div>
105+
</div>
106+
</td>
107+
<td class='hidden lg:table-cell font-light py-2 px-4'>
108+
{{ job.get('duration') or '' }}
109+
</td>
110+
<td class='hidden md:table-cell font-light py-2 px-4'>
111+
{{ job.get('cost') or '' }}
112+
</td>
113+
</tr>
114+
{% endfor %}
115+
</tbody>
116+
</table>
117+
</div>
118+
{% if last_job_id is not none %}
119+
<div class='pt-2 flex w-full justify-end'>
120+
<form method="GET" action="{{ base_path }}/batches/{{ batch['id'] }}">
121+
{% if q is not none %}
122+
<input type="hidden" name="q" value="{{ q }}" />
123+
{% endif %}
124+
<input type="hidden" name="last_job_id" value="{{ last_job_id }}" />
125+
{{ submit_button('Next page') }}
126+
</form>
127+
</div>
118128
{% endif %}
119-
<input type="hidden" name="last_job_id" value="{{ last_job_id }}" />
120-
<button>Next page</button>
121-
</form>
122-
{% endif %}
129+
</div>
123130
</div>
124131
{% endblock %}

0 commit comments

Comments
 (0)