-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathstart_page.py
209 lines (166 loc) · 7.19 KB
/
start_page.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""Module to generate AiiDAlab home page."""
import json
from functools import wraps
from glob import glob
from os import path
import ipywidgets as ipw
import traitlets
from aiidalab.app import AiidaLabApp
from aiidalab.config import AIIDALAB_APPS
from IPython.display import display
from home.utils import load_widget
from home.widgets import AppStatusInfoWidget
def create_app_widget_move_buttons(name):
"""Make buttons to move the app widget up or down."""
layout = ipw.Layout(width="40px")
app_widget_move_buttons = ipw.HTML(
f"""
<a href=./start.ipynb?move_up={name} title="Move it up"><i class='fa fa-arrow-up' style='color:#337ab7;font-size:2em;' ></i></a>
<a href=./start.ipynb?move_down={name} title="Move it down"><i class='fa fa-arrow-down' style='color:#337ab7;font-size:2em;' ></i></a>
""",
layout=layout,
)
app_widget_move_buttons.layout.margin = "50px 0px 0px 0px"
return app_widget_move_buttons
def _workaround_property_lock_issue(func):
"""Work-around for issue with the ipw.Accordion widget.
The widget does not report changes to the .selected_index trait when displayed
within a custom ipw.Output instance. However, the change is somewhat cryptic reported
by a change to the private '_property_lock' trait. We observe changes to that trait
and convert the change argument into a form that is more like the one expected by
downstream handlers.
"""
@wraps(func)
def _inner(self, change):
if change["name"] == "_property_lock":
if "selected_index" in change["old"]:
fixed_change = change.copy()
fixed_change["name"] = "selected_index"
fixed_change["new"] = change["old"]["selected_index"]
del fixed_change["old"]
return func(self, fixed_change)
return func(self, change)
return _inner
class AiidaLabHome:
"""Class that mananges the appearance of the AiiDAlab home page."""
def __init__(self):
self.config_fn = ".launcher.json"
self.output = ipw.Output()
self._app_widgets = dict()
def _create_app_widget(self, name):
"""Create the widget representing the app on the home screen."""
config = self.read_config()
app = AiidaLabApp(name, None, AIIDALAB_APPS)
if name == "home":
app_widget = AppWidget(app, allow_move=False, allow_manage=False)
else:
app_widget = CollapsableAppWidget(app, allow_move=True)
app_widget.hidden = name in config["hidden"]
app_widget.observe(self._on_app_widget_change_hidden, names=["hidden"])
return app_widget
def _on_app_widget_change_hidden(self, change):
"""Record whether a app widget is hidden on the home screen in the config file."""
config = self.read_config()
hidden = set(config["hidden"])
if change["new"]: # hidden
hidden.add(change["owner"].app.name)
else: # visible
hidden.discard(change["owner"].app.name)
config["hidden"] = list(hidden)
self.write_config(config)
def write_config(self, config):
json.dump(config, open(self.config_fn, "w"), indent=2)
def read_config(self):
if path.exists(self.config_fn):
return json.load(open(self.config_fn, "r"))
return {"order": [], "hidden": []} # default config
def render(self):
"""Rendering all apps."""
self.output.clear_output()
apps = self.load_apps()
with self.output:
for name in apps:
# Create app widget if it has not been created yet.
if name not in self._app_widgets:
self._app_widgets[name] = self._create_app_widget(name)
display(self._app_widgets[name])
return self.output
def load_apps(self):
"""Load apps according to the order defined in the config file."""
apps = [
path.basename(fn)
for fn in glob(path.join(AIIDALAB_APPS, "*"))
if path.isdir(fn)
and not fn.endswith("home")
and not fn.endswith("__pycache__")
]
config = self.read_config()
order = config["order"]
apps.sort(key=lambda x: order.index(x) if x in order else -1)
config["order"] = apps
self.write_config(config)
return ["home"] + apps
def move_updown(self, name, delta):
"""Move the app up/down on the start page."""
config = self.read_config()
order = config["order"]
i = order.index(name)
del order[i]
j = min(len(order), max(0, i + delta))
order.insert(j, name)
config["order"] = order
self.write_config(config)
class AppWidget(ipw.VBox):
"""Widget that represents an app as part of the home page."""
def __init__(self, app, allow_move=False, allow_manage=True):
self.app = app
launcher = load_widget(app.name)
launcher.layout = ipw.Layout(width="900px")
header_items = []
footer_items = []
if allow_manage:
app_status_info = AppStatusInfoWidget()
for trait in ("detached", "compatible", "remote_update_status"):
ipw.dlink((app, trait), (app_status_info, trait))
app_status_info.layout.margin = "0px 0px 0px 800px"
header_items.append(app_status_info)
footer_items.append(
f"""<a href=./single_app.ipynb?app={app.name} target="_blank"><button>Manage App</button></a>"""
)
if app.metadata.get("external_url"):
footer_items.append(
f"""<a href="{app.metadata['external_url']}" target="_blank"><button>URL</button></a>"""
)
if allow_move:
app_widget_move_buttons = create_app_widget_move_buttons(app.name)
body = ipw.HBox([launcher, app_widget_move_buttons])
else:
body = launcher
header = ipw.HBox(header_items)
header.layout.margin = None if allow_manage else "20px 0px 0px 0px"
footer = ipw.HTML(" ".join(footer_items), layout={"width": "initial"})
footer.layout.margin = (
"0px 0px 0px 700px" if allow_manage else "0px 0px 20px 0px"
)
super().__init__(children=[header, body, footer])
class CollapsableAppWidget(ipw.Accordion):
"""Widget that represents a collapsable app as part of the home page."""
hidden = traitlets.Bool()
def __init__(self, app, **kwargs):
self.app = app
app_widget = AppWidget(app, **kwargs)
super().__init__(children=[app_widget])
self.set_title(0, app.title)
# Need to observe all names here due to unidentified issue:
self.observe(
self._observe_accordion_selected_index
) # , names=['selected_index'])
@_workaround_property_lock_issue
def _observe_accordion_selected_index(self, change):
if (
change["name"] == "selected_index"
): # Observing all names due to unidentified issue.
self.hidden = change["new"] is None
@traitlets.observe("hidden")
def _observe_hidden(self, change):
self.selected_index = None if change["new"] else 0