diff --git a/consts.py b/consts.py index bb162d0..eb001de 100644 --- a/consts.py +++ b/consts.py @@ -1,13 +1,20 @@ +class Status: + GOOD = 'good' + DOWN = 'down' + UNKNOWN = 'unknown-device' + BROKEN = 'broken' + MISCONFIGURED = 'misconfigured' + STATUSES = \ - ['good'] * 20 + \ - ['down'] * 2 + \ - ['unknown-device'] * 5 + \ - ['broken', 'misconfigured'] + [Status.GOOD] * 20 + \ + [Status.DOWN] * 2 + \ + [Status.UNKNOWN] * 5 + \ + [Status.BROKEN, Status.MISCONFIGURED] COLORS = { - 'good': '#72fa93', - 'down': '#e45f2b', - 'unknown-device': '#f6c445', - 'broken': '#e39af0', - 'misconfigured': '#9ac1f0', + Status.GOOD: '#72fa93', + Status.DOWN: '#e45f2b', + Status.UNKNOWN: '#f6c445', + Status.BROKEN: '#e39af0', + Status.MISCONFIGURED: '#9ac1f0', } diff --git a/css_utils.py b/css_utils.py index 5d6c166..bf44af3 100644 --- a/css_utils.py +++ b/css_utils.py @@ -1,3 +1,6 @@ +from reactpy import html + + def grid_position(x, y, width=1, height=1): width_str = height_str = '' if width != 1: @@ -8,3 +11,10 @@ def grid_position(x, y, width=1, height=1): 'grid-row': f'{y} {height_str}', 'grid-column': f'{x} {width_str}', } + + +def colorize(text: str, color: str): + return html.span( + {'style': {'color': color}}, + text, + ) diff --git a/static/dark.css b/static/dark.css index ac4c554..02f184c 100755 --- a/static/dark.css +++ b/static/dark.css @@ -135,12 +135,40 @@ div#app { border-radius: 5px; background-color: #101014; box-shadow: 0px 0px 3px #0f0f11; + position: absolute; + left: -100%; + padding: 10px; + z-index: 2; +} + +.vl-popup { + border-radius: 5px; + background-color: #101014; + box-shadow: 0px 0px 5px #c5ced1; + position: absolute; left: -100%; padding: 10px; z-index: 1; } +.vl-popup-button { + border-radius: 5px; + background-color: #1f242e; + outline-style: solid; + outline-width: 1px; + outline-color: #ffffffaa; + padding: 2px; +} + +.vl-popup-button:hover { + background-color: #161920; + outline-style: solid; + outline-width: 1px; + outline-color: #ffffff33; + padding: 2px; +} + @keyframes appear-vertical { from { height: 0px; diff --git a/visual_lab.py b/visual_lab.py index c56d118..04688dc 100644 --- a/visual_lab.py +++ b/visual_lab.py @@ -68,6 +68,7 @@ def on_click(title: str, cell_number: int): def clear_focused_cell(_): set_focused_cell(None) + set_hovered_cell(None) return html.div( { diff --git a/widgets/cell.py b/widgets/cell.py index cb91dd0..3bbe934 100644 --- a/widgets/cell.py +++ b/widgets/cell.py @@ -3,8 +3,9 @@ from reactpy import html, component, use_effect, use_ref, Ref, event -from css_utils import grid_position -from .tooltip import Tooltip +from css_utils import grid_position, colorize +from . import tooltip +from .popup import generate_popup from consts import COLORS STATUS_BAR_DELAY_OFFSET = 0.17 # trust me on this one @@ -26,6 +27,10 @@ class CellDetails: on_click: Callable[[], None] on_hover: Callable[[bool], None] + @property + def cell_id(self) -> str: + return f'{self.cabinet}-{self.number}' + @component def StatusBar(delay: float, should_animate: bool): @@ -70,22 +75,37 @@ async def effect(): {'class_name': 'cell-text'}, details.number ), - StatusBar(STATUS_BAR_DELAY_OFFSET + details.delay, should_animate.current) + StatusBar(STATUS_BAR_DELAY_OFFSET + + details.delay, should_animate.current) ) + popup = None + if details.show_popup: + popup = CellPopup(details) + elif details.show_tooltip: + popup = CellTooltip(details) + if popup is not None: + return html.div(cell, popup) - if details.show_tooltip or details.show_popup: - return CellTooltip(details, hoverables=[cell]) return cell @component -def CellTooltip(details: CellDetails, hoverables): - tooltip = html.div( +def CellPopup(details: CellDetails): + contents = html.div( + { + # Clicking the popup should not make it disappear + 'onclick': event(lambda _: None, stop_propagation=True), + }, + generate_popup(details), + ) + return tooltip.Tooltip(contents, class_name=tooltip.POPUP) + + +@component +def CellTooltip(details: CellDetails): + contents = html.div( {'style': {'width': '130px'}}, f"Device at {details.cabinet}-{details.number} has status ", - html.span( - {'style': {'color': COLORS[details.status]}}, - details.status, - ) + colorize(details.status, COLORS[details.status]), ) - return Tooltip(tooltip, hoverables) + return tooltip.Tooltip(contents) diff --git a/widgets/popup.py b/widgets/popup.py new file mode 100644 index 0000000..65cb57e --- /dev/null +++ b/widgets/popup.py @@ -0,0 +1,79 @@ +from reactpy import component, html +from reactpy.types import Component +from typing import TypeVar, Callable, cast +from . import cell +from .tooltip import PopupButton +from consts import Status, COLORS +from css_utils import colorize + +POPUP_WIDTH = '250px' + +PopupMaker = TypeVar( + 'PopupMaker', + bound=Callable[['cell.CellDetails'], Component] +) +_POPUPS: dict[str, PopupMaker] = {} + + +@component +def default_handler(details: 'cell.CellDetails'): + return html.div( + { + 'style': { + 'width': POPUP_WIDTH, + } + }, + colorize('Error', '#f12323'), + f': popup for status {details.status} has not been implemented yet!' + ) + + +def get_handler(status: str) -> PopupMaker: + return _POPUPS.get(status, default_handler) + + +def generate_popup(details: 'cell.CellDetails'): + handler = get_handler(details.status) + return handler(details) + + +def popup_maker(status: str) -> Callable[[PopupMaker], PopupMaker]: + def wrapper(func: PopupMaker) -> PopupMaker: + _POPUPS[status] = func + return func + return wrapper + + +@popup_maker(Status.DOWN) +@component +def _popup_down(details: 'cell.CellDetails'): + return html.div( + { + 'style': { + 'width': POPUP_WIDTH, + }, + }, + f'The interface in cell {details.cell_id} is ', + colorize('DOWN', COLORS[Status.DOWN]), + '. Please make sure the cable is connected!', + ) + +@popup_maker(Status.MISCONFIGURED) +@component +def _popup_misconfigured(details: 'cell.CellDetails'): + # TODO: Improve the styling on the button in this component + def quick_fix(_): + # TODO: Reconfigure the DB + ... + + return html.div( + { + 'style': { + 'width': POPUP_WIDTH, + }, + }, + # TODO: Fix this message + f'Device #100 is in this cell, but is configured to cell A-1.\n', + f'Click the button below to move it to {details.cell_id}\n', + PopupButton('Quick Fix', quick_fix), + ) diff --git a/widgets/tooltip.py b/widgets/tooltip.py index 21d7426..fe582e0 100644 --- a/widgets/tooltip.py +++ b/widgets/tooltip.py @@ -1,23 +1,37 @@ +from typing import Callable from reactpy import html, component from reactpy.types import Component +TOOLTIP = 'vl-tooltip' +POPUP = 'vl-popup' @component -def Tooltip(tooltip_content: Component, hoverables): - if not hoverables: - return html.span() - - tooltip = html.div( - {'class_name': 'vl-tooltip'}, - tooltip_content - ) - +def Tooltip(tooltip_content: Component, class_name=TOOLTIP): return html.div( { 'style': { 'position': 'relative', } }, - *hoverables, - tooltip + html.div( + { + 'class_name': class_name + }, + tooltip_content + ) + ) + + +@component +def PopupButton(text: str, onclick: Callable, override_style=None): + style = {} + if override_style: + style.update(override_style) + return html.span( + { + 'class_name': 'vl-popup-button', + 'onclick': onclick, + 'style': style, + }, + f' {text} ' )