From 238c230a4134b9c11d50b6be3d47e76abc5667f2 Mon Sep 17 00:00:00 2001 From: Geoffrey Yu Date: Tue, 12 Mar 2024 18:45:30 -0400 Subject: [PATCH] Visualize table placement relationships using hover state (#479) * Set mapping values * Add UI support for table hover * Hover state improvements * Check in * Fix hover bug * Hide non-matching * Hoist relevant tables * Apply hover effects to engines too * Format * Status fix --- src/brad/ui/manager_impl.py | 44 +++++++++++++-- src/brad/ui/models.py | 2 +- ui/src/App.css | 5 ++ ui/src/App.jsx | 48 +++++++++++++++- ui/src/components/BlueprintView.jsx | 21 ++++++- ui/src/components/Header.jsx | 5 +- ui/src/components/Panel.jsx | 4 +- ui/src/components/PhysDbView.jsx | 40 +++++++++++-- ui/src/components/TableView.jsx | 15 ++++- ui/src/components/VdbeView.jsx | 37 ++++++++++-- ui/src/components/VirtualInfraView.jsx | 24 +++++--- ui/src/components/styles/DbCylinder.css | 2 + ui/src/components/styles/PhysDbView.css | 5 ++ ui/src/components/styles/TableView.css | 18 ++++++ ui/src/components/styles/VdbeView.css | 6 ++ ui/src/highlight.js | 75 +++++++++++++++++++++++++ 16 files changed, 317 insertions(+), 34 deletions(-) create mode 100644 ui/src/highlight.js diff --git a/src/brad/ui/manager_impl.py b/src/brad/ui/manager_impl.py index 9ac5a7d3..99a31bd6 100644 --- a/src/brad/ui/manager_impl.py +++ b/src/brad/ui/manager_impl.py @@ -7,7 +7,9 @@ from typing import Optional, List import brad.ui.static as brad_app +from brad.blueprint import Blueprint from brad.blueprint.manager import BlueprintManager +from brad.config.engine import Engine from brad.config.file import ConfigFile from brad.daemon.monitor import Monitor from brad.ui.uvicorn_server import PatchedUvicornServer @@ -95,7 +97,7 @@ def get_system_state() -> SystemState: txn_tables = ["theatres", "showings", "ticket_orders", "movie_info", "aka_title"] txn_only = ["theatres", "showings", "ticket_orders"] vdbe1 = DisplayableVirtualEngine( - index=1, + name="VDBE 1", freshness="Serializable", dialect="PostgreSQL SQL", peak_latency_s=0.030, @@ -112,12 +114,16 @@ def get_system_state() -> SystemState: ) vdbe1.tables.sort(key=lambda t: t.name) vdbe2 = DisplayableVirtualEngine( - index=2, + name="VDBE 2", freshness="≤ 10 minutes stale", dialect="PostgreSQL SQL", peak_latency_s=30.0, tables=[ - DisplayableTable(name=table.name, is_writer=False, mapped_to=[]) + DisplayableTable( + name=table.name, + is_writer=False, + mapped_to=_analytics_table_mapper_temp(table.name, blueprint), + ) for table in blueprint.tables() if table.name not in txn_only ], @@ -130,8 +136,36 @@ def get_system_state() -> SystemState: if t.name in txn_tables: t.is_writer = True virtual_infra = VirtualInfrastructure(engines=[vdbe1, vdbe2]) - - return SystemState(virtual_infra=virtual_infra, blueprint=dbp) + system_state = SystemState(virtual_infra=virtual_infra, blueprint=dbp) + _add_reverse_mapping_temp(system_state) + return system_state + + +def _analytics_table_mapper_temp(table_name: str, blueprint: Blueprint) -> List[str]: + # TODO: This is a hard-coded heurstic for the mock up only. + locations = blueprint.get_table_locations(table_name) + names = [] + if Engine.Redshift in locations: + names.append("Redshift") + if Engine.Athena in locations: + names.append("Athena") + return names + + +def _add_reverse_mapping_temp(system_state: SystemState) -> None: + # TODO: This is a hard-coded heuristic for the mock up only. + # This mutates the passed-in object. + veng_tables = {} + for veng in system_state.virtual_infra.engines: + table_names = {table.name for table in veng.tables} + veng_tables[veng.name] = table_names + + for engine in system_state.blueprint.engines: + for table in engine.tables: + name = table.name + for veng_name, tables in veng_tables.items(): + if name in tables: + table.mapped_to.append(veng_name) @app.get("/api/1/system_events") diff --git a/src/brad/ui/models.py b/src/brad/ui/models.py index d5293ac8..9764beb0 100644 --- a/src/brad/ui/models.py +++ b/src/brad/ui/models.py @@ -84,7 +84,7 @@ def from_blueprint(cls, blueprint: Blueprint) -> "DisplayableBlueprint": class DisplayableVirtualEngine(BaseModel): - index: int + name: str freshness: str dialect: str peak_latency_s: Optional[float] = None diff --git a/ui/src/App.css b/ui/src/App.css index ddd05d04..14626ef6 100644 --- a/ui/src/App.css +++ b/ui/src/App.css @@ -67,6 +67,11 @@ body { box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); } +.infra-column-panel { + flex-basis: 50%; + flex-grow: 1; +} + /* Responsive layout */ @media only screen and (max-width: 800px) { .body-container { diff --git a/ui/src/App.jsx b/ui/src/App.jsx index aafd011c..59315c0f 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -14,6 +14,40 @@ function App() { blueprint: null, virtual_infra: null, }); + const [highlight, setHighlight] = useState({ + hoverEngine: null, + virtualEngines: {}, + physicalEngines: {}, + }); + + const onTableHoverEnter = (engineMarker, tableName, isVirtual, mappedTo) => { + const virtualEngines = {}; + const physicalEngines = {}; + if (isVirtual) { + virtualEngines[engineMarker] = tableName; + for (const physMarker of mappedTo) { + physicalEngines[physMarker] = tableName; + } + } else { + physicalEngines[engineMarker] = tableName; + for (const virtMarker of mappedTo) { + virtualEngines[virtMarker] = tableName; + } + } + setHighlight({ + hoverEngine: engineMarker, + virtualEngines, + physicalEngines, + }); + }; + + const onTableHoverExit = () => { + setHighlight({ + hoverEngine: null, + virtualEngines: {}, + physicalEngines: {}, + }); + }; // Fetch updated system state periodically. useEffect(() => { @@ -44,8 +78,18 @@ function App() {

Data Infrastructure

- - + +
diff --git a/ui/src/components/BlueprintView.jsx b/ui/src/components/BlueprintView.jsx index d9f94652..cf276d15 100644 --- a/ui/src/components/BlueprintView.jsx +++ b/ui/src/components/BlueprintView.jsx @@ -2,14 +2,29 @@ import Panel from "./Panel"; import PhysDbView from "./PhysDbView"; import "./styles/BlueprintView.css"; -function BlueprintView({ blueprint }) { +function BlueprintView({ + blueprint, + highlight, + onTableHoverEnter, + onTableHoverExit, +}) { return ( - +
{blueprint && blueprint.engines && blueprint.engines.map(({ name, ...props }) => ( - + ))}
diff --git a/ui/src/components/Header.jsx b/ui/src/components/Header.jsx index ac3c92cc..28f61f28 100644 --- a/ui/src/components/Header.jsx +++ b/ui/src/components/Header.jsx @@ -34,10 +34,7 @@ function Header() { BRAD Dashboard - + ); diff --git a/ui/src/components/Panel.jsx b/ui/src/components/Panel.jsx index 64ce1853..c64a9605 100644 --- a/ui/src/components/Panel.jsx +++ b/ui/src/components/Panel.jsx @@ -1,8 +1,8 @@ import "./styles/Panel.css"; -function Panel({ heading, children }) { +function Panel({ heading, children, className }) { return ( -
+
{heading &&

{heading}

} {children}
diff --git a/ui/src/components/PhysDbView.jsx b/ui/src/components/PhysDbView.jsx index 62e11223..406ef9d3 100644 --- a/ui/src/components/PhysDbView.jsx +++ b/ui/src/components/PhysDbView.jsx @@ -1,15 +1,47 @@ import DbCylinder from "./DbCylinder"; import TableView from "./TableView"; import "./styles/PhysDbView.css"; +import { + highlightTableViewClass, + highlightEngineViewClass, + sortTablesToHoist, +} from "../highlight"; + +function PhysDbView({ + name, + provisioning, + tables, + highlight, + onTableHoverEnter, + onTableHoverExit, +}) { + const physDbName = name; + const sortedTables = sortTablesToHoist(highlight, physDbName, false, tables); -function PhysDbView({ name, provisioning, tables }) { return ( -
+
{name}
{provisioning}
- {tables.map(({ name, is_writer }) => ( - + {sortedTables.map(({ name, is_writer, mapped_to }) => ( + + onTableHoverEnter(physDbName, name, false, mapped_to) + } + onTableHoverExit={onTableHoverExit} + /> ))}
diff --git a/ui/src/components/TableView.jsx b/ui/src/components/TableView.jsx index 175a7b6f..3023b333 100644 --- a/ui/src/components/TableView.jsx +++ b/ui/src/components/TableView.jsx @@ -4,9 +4,20 @@ function WriterMarker({ color }) { return
W
; } -function TableView({ name, isWriter, color }) { +function TableView({ + name, + isWriter, + color, + onTableHoverEnter, + onTableHoverExit, + highlightClass, +}) { return ( -
+
{name} {isWriter && }
diff --git a/ui/src/components/VdbeView.jsx b/ui/src/components/VdbeView.jsx index 7faaadad..0c02940a 100644 --- a/ui/src/components/VdbeView.jsx +++ b/ui/src/components/VdbeView.jsx @@ -1,11 +1,30 @@ import DbCylinder from "./DbCylinder"; import TableView from "./TableView"; import "./styles/VdbeView.css"; +import { + highlightTableViewClass, + highlightEngineViewClass, + sortTablesToHoist, +} from "../highlight"; + +function VdbeView({ + name, + freshness, + dialect, + peak_latency_s, + tables, + highlight, + onTableHoverEnter, + onTableHoverExit, +}) { + const vengName = name; + const sortedTables = sortTablesToHoist(highlight, vengName, true, tables); -function VdbeView({ name, freshness, dialect, peak_latency_s, tables }) { return ( -
- {name} +
+ {vengName}
  • 🌿: {freshness}
  • @@ -14,12 +33,22 @@ function VdbeView({ name, freshness, dialect, peak_latency_s, tables }) {
- {tables.map(({ name, is_writer }) => ( + {sortedTables.map(({ name, is_writer, mapped_to }) => ( + onTableHoverEnter(vengName, name, true, mapped_to) + } + onTableHoverExit={onTableHoverExit} /> ))}
diff --git a/ui/src/components/VirtualInfraView.jsx b/ui/src/components/VirtualInfraView.jsx index 20262f2c..c742fab7 100644 --- a/ui/src/components/VirtualInfraView.jsx +++ b/ui/src/components/VirtualInfraView.jsx @@ -2,15 +2,25 @@ import Panel from "./Panel"; import VdbeView from "./VdbeView"; import "./styles/VirtualInfraView.css"; -function VirtualInfraView({ virtualInfra }) { +function VirtualInfraView({ + virtualInfra, + highlight, + onTableHoverEnter, + onTableHoverExit, +}) { return ( - +
- {virtualInfra && - virtualInfra.engines && - virtualInfra.engines.map(({ index, ...props }) => ( - - ))} + {virtualInfra?.engines?.map(({ name, ...props }) => ( + + ))}
); diff --git a/ui/src/components/styles/DbCylinder.css b/ui/src/components/styles/DbCylinder.css index 07ba5b91..9fdd7006 100644 --- a/ui/src/components/styles/DbCylinder.css +++ b/ui/src/components/styles/DbCylinder.css @@ -9,6 +9,8 @@ opacity: 0.85; transition: opacity 0.5s; + + cursor: default; } .db-cylinder:hover { diff --git a/ui/src/components/styles/PhysDbView.css b/ui/src/components/styles/PhysDbView.css index 68662602..43969642 100644 --- a/ui/src/components/styles/PhysDbView.css +++ b/ui/src/components/styles/PhysDbView.css @@ -12,3 +12,8 @@ font-weight: 600; height: 30px; } + +.physdb-view.dim .db-cylinder, +.physdb-view.dim .physdb-view-prov { + opacity: 0.3; +} diff --git a/ui/src/components/styles/TableView.css b/ui/src/components/styles/TableView.css index 2a96767a..2a3375ee 100644 --- a/ui/src/components/styles/TableView.css +++ b/ui/src/components/styles/TableView.css @@ -17,6 +17,24 @@ border-radius: 3px; margin: 8px 0; position: relative; + transition: + opacity 0.3s, + margin 0.2s, + height 0.2s, + visibility 0s; + cursor: default; +} + +.db-table-view.dim { + opacity: 0.2; +} + +.db-table-view.highlight { + opacity: 1; +} + +.db-table-view.hidden { + opacity: 0.2; } .db-table-view-writer { diff --git a/ui/src/components/styles/VdbeView.css b/ui/src/components/styles/VdbeView.css index 282803e1..0fc586d1 100644 --- a/ui/src/components/styles/VdbeView.css +++ b/ui/src/components/styles/VdbeView.css @@ -6,8 +6,14 @@ margin-top: 10px; } +.vdbe-view.dim .vdbe-view-props, +.vdbe-view.dim .db-cylinder { + opacity: 0.3; +} + .vdbe-view-props { margin: 0 0 15px 0; + transition: opacity 0.3s; } .vdbe-view-props ul { diff --git a/ui/src/highlight.js b/ui/src/highlight.js new file mode 100644 index 00000000..98c53b41 --- /dev/null +++ b/ui/src/highlight.js @@ -0,0 +1,75 @@ +function highlightTableViewClass( + highlightState, + engineName, + tableName, + isVirtual, +) { + if (highlightState.hoverEngine == null) { + return ""; + } + const relevantState = isVirtual + ? highlightState.virtualEngines + : highlightState.physicalEngines; + const shouldHighlight = relevantState[engineName] === tableName; + if (shouldHighlight) { + return "highlight"; + } else { + if (highlightState.hoverEngine === engineName) { + return "dim"; + } else { + return "hidden"; + } + } +} + +function highlightEngineViewClass(highlightState, engineName, isVirtual) { + if (highlightState.hoverEngine == null) { + return ""; + } + const relevantState = isVirtual + ? highlightState.virtualEngines + : highlightState.physicalEngines; + const shouldHighlight = + relevantState[engineName] != null || + highlightState.hoverEngine === engineName; + if (shouldHighlight) { + return "highlight"; + } else { + return "dim"; + } +} + +function sortTablesToHoist(highlightState, currentEngine, isVirtual, tables) { + const tableCopy = tables.slice(); + if ( + highlightState.hoverEngine == null || + highlightState.hoverEngine === currentEngine + ) { + return tableCopy; + } + + let relTables = null; + if (isVirtual) { + relTables = highlightState.virtualEngines; + } else { + relTables = highlightState.physicalEngines; + } + if (relTables[currentEngine] == null) { + return tableCopy; + } + + let hoistIndex = null; + tableCopy.forEach(({ name }, index) => { + if (name === relTables[currentEngine]) { + hoistIndex = index; + } + }); + if (hoistIndex != null && tableCopy.length > 0) { + const tmp = tableCopy[0]; + tableCopy[0] = tableCopy[hoistIndex]; + tableCopy[hoistIndex] = tmp; + } + return tableCopy; +} + +export { highlightTableViewClass, highlightEngineViewClass, sortTablesToHoist };