From 5a0476a470437dde627e20c77a83d6ef228166be Mon Sep 17 00:00:00 2001 From: Geoffrey Yu Date: Sat, 25 Jan 2025 19:35:36 -0500 Subject: [PATCH] Set up VDBE create, edit, delete APIs and connect to dashboard, other UI enhancements (#519) --- config/vdbe_demo/imdb_editable_vdbes.json | 222 ++++++++++++++++++ config/vdbe_demo/imdb_extended_vdbes.json | 10 +- src/brad/admin/modify_blueprint.py | 18 ++ src/brad/daemon/daemon.py | 3 +- src/brad/ui/manager_impl.py | 76 +++--- src/brad/ui/models.py | 54 ++++- src/brad/vdbe/manager.py | 83 ++++++- src/brad/vdbe/models.py | 13 +- tools/serialize_vdbes.py | 2 + ui/src/App.jsx | 8 + ui/src/api.js | 18 ++ ui/src/components/BlueprintView.jsx | 76 +++--- ui/src/components/ConfirmDialog.jsx | 23 ++ ui/src/components/CreateEditVdbeForm.jsx | 146 +++++++++--- ui/src/components/HighlightContext.jsx | 13 + ui/src/components/HighlightablePhysDb.jsx | 20 ++ ui/src/components/HighlightableVdbe.jsx | 20 ++ ui/src/components/OverallInfraView.jsx | 186 ++++++++------- ui/src/components/PerfView.jsx | 40 +--- ui/src/components/PhysDbView.jsx | 47 +--- ui/src/components/TableView.jsx | 12 +- ui/src/components/VdbeMetricsView.jsx | 20 ++ ui/src/components/VdbeView.jsx | 103 ++++---- ui/src/components/VirtualInfraView.jsx | 13 +- .../components/styles/CreateEditVdbeForm.css | 6 + ui/src/components/styles/PerfView.css | 1 + ui/src/components/styles/PhysDbView.css | 6 +- ui/src/components/styles/VdbeView.css | 8 +- ui/src/components/styles/VirtualInfraView.css | 4 +- ui/src/highlight.js | 38 ++- 30 files changed, 935 insertions(+), 354 deletions(-) create mode 100644 config/vdbe_demo/imdb_editable_vdbes.json create mode 100644 ui/src/components/ConfirmDialog.jsx create mode 100644 ui/src/components/HighlightContext.jsx create mode 100644 ui/src/components/HighlightablePhysDb.jsx create mode 100644 ui/src/components/HighlightableVdbe.jsx create mode 100644 ui/src/components/VdbeMetricsView.jsx diff --git a/config/vdbe_demo/imdb_editable_vdbes.json b/config/vdbe_demo/imdb_editable_vdbes.json new file mode 100644 index 00000000..d95beec2 --- /dev/null +++ b/config/vdbe_demo/imdb_editable_vdbes.json @@ -0,0 +1,222 @@ +{ + "schema_name": "imdb_extended_editable_100g", + "engines": [ + { + "internal_id": 1, + "name": "Ticketing", + "max_staleness_ms": 0, + "p90_latency_slo_ms": 30, + "interface": "postgresql", + "tables": [ + { + "name": "theatres", + "writable": true + }, + { + "name": "showings", + "writable": true + }, + { + "name": "ticket_orders", + "writable": true + }, + { + "name": "movie_info", + "writable": true + }, + { + "name": "aka_title", + "writable": true + } + ], + "mapped_to": "aurora" + }, + { + "internal_id": 2, + "name": "Analytics", + "max_staleness_ms": 3600000, + "p90_latency_slo_ms": 30000, + "interface": "postgresql", + "tables": [ + { + "name": "homes", + "writable": false + }, + { + "name": "theatres", + "writable": false + }, + { + "name": "showings", + "writable": false + }, + { + "name": "ticket_orders", + "writable": false + }, + { + "name": "aka_name", + "writable": false + }, + { + "name": "aka_title", + "writable": false + }, + { + "name": "cast_info", + "writable": false + }, + { + "name": "char_name", + "writable": false + }, + { + "name": "comp_cast_type", + "writable": false + }, + { + "name": "company_name", + "writable": false + }, + { + "name": "company_type", + "writable": false + }, + { + "name": "complete_cast", + "writable": false + }, + { + "name": "info_type", + "writable": false + }, + { + "name": "keyword", + "writable": false + }, + { + "name": "kind_type", + "writable": false + }, + { + "name": "link_type", + "writable": false + }, + { + "name": "movie_companies", + "writable": false + }, + { + "name": "movie_info_idx", + "writable": false + }, + { + "name": "movie_keyword", + "writable": false + }, + { + "name": "movie_link", + "writable": false + }, + { + "name": "name", + "writable": false + }, + { + "name": "role_type", + "writable": false + }, + { + "name": "title", + "writable": false + }, + { + "name": "movie_info", + "writable": false + }, + { + "name": "person_info", + "writable": false + } + ], + "mapped_to": "redshift" + } + ], + "tables": [ + { + "name": "homes" + }, + { + "name": "theatres" + }, + { + "name": "showings" + }, + { + "name": "ticket_orders" + }, + { + "name": "aka_name" + }, + { + "name": "aka_title" + }, + { + "name": "cast_info" + }, + { + "name": "char_name" + }, + { + "name": "comp_cast_type" + }, + { + "name": "company_name" + }, + { + "name": "company_type" + }, + { + "name": "complete_cast" + }, + { + "name": "info_type" + }, + { + "name": "keyword" + }, + { + "name": "kind_type" + }, + { + "name": "link_type" + }, + { + "name": "movie_companies" + }, + { + "name": "movie_info_idx" + }, + { + "name": "movie_keyword" + }, + { + "name": "movie_link" + }, + { + "name": "name" + }, + { + "name": "role_type" + }, + { + "name": "title" + }, + { + "name": "movie_info" + }, + { + "name": "person_info" + } + ] +} diff --git a/config/vdbe_demo/imdb_extended_vdbes.json b/config/vdbe_demo/imdb_extended_vdbes.json index f37a78cc..96cfa1d9 100644 --- a/config/vdbe_demo/imdb_extended_vdbes.json +++ b/config/vdbe_demo/imdb_extended_vdbes.json @@ -1,8 +1,9 @@ { - "schema_name": "imdb_extended", + "schema_name": "imdb_extended_100g", "engines": [ { - "name": "VDBE (T)", + "internal_id": 1, + "name": "Ticketing", "max_staleness_ms": 0, "p90_latency_slo_ms": 30, "interface": "postgresql", @@ -31,7 +32,8 @@ "mapped_to": "aurora" }, { - "name": "VDBE (A)", + "internal_id": 2, + "name": "Analytics", "max_staleness_ms": 3600000, "p90_latency_slo_ms": 30000, "interface": "postgresql", @@ -137,7 +139,7 @@ "writable": false } ], - "mapped_to": "redshift" + "mapped_to": "aurora" } ], "tables": [ diff --git a/src/brad/admin/modify_blueprint.py b/src/brad/admin/modify_blueprint.py index 2e3db4c8..0813c98e 100644 --- a/src/brad/admin/modify_blueprint.py +++ b/src/brad/admin/modify_blueprint.py @@ -22,6 +22,7 @@ from brad.routing.policy import RoutingPolicy from brad.routing.tree_based.forest_policy import ForestPolicy from brad.routing.rule_based import RuleBased +from brad.vdbe.models import VirtualInfrastructure logger = logging.getLogger(__name__) @@ -102,6 +103,11 @@ def register_admin_action(subparser) -> None: "on the specified engines. Overridden by --place-tables-everywhere. Format " "argument as a string of the form: table1=engine1,engine2;table2=engine3;", ) + parser.add_argument( + "--place-vdbe-tables", + type=str, + help="Places tables on the specified engines in the given VDBE JSON file.", + ) parser.add_argument( "--set-routing-policy", choices=[ @@ -323,6 +329,18 @@ def modify_blueprint(args) -> None: new_placement[tbl] = Engine.from_bitmap(Engine.bitmap_all()) enum_blueprint.set_table_locations(new_placement) + if args.place_vdbe_tables is not None: + new_placement = {} + with open(args.place_vdbe_tables, "r", encoding="utf-8") as f: + vinfra = VirtualInfrastructure.model_validate_json(f.read()) + for engine in vinfra.engines: + for table in engine.tables: + try: + new_placement[table.name].append(engine.mapped_to) + except KeyError: + new_placement[table.name] = [engine.mapped_to] + enum_blueprint.set_table_locations(new_placement) + # 5. Modify routing policy as needed. if args.set_routing_policy is not None: if args.set_routing_policy == "always_redshift": diff --git a/src/brad/daemon/daemon.py b/src/brad/daemon/daemon.py index 1c37d947..e1f5fb5f 100644 --- a/src/brad/daemon/daemon.py +++ b/src/brad/daemon/daemon.py @@ -132,7 +132,8 @@ def __init__( load_vdbe_path = self._config.bootstrap_vdbe_path() if load_vdbe_path is not None: self._vdbe_manager: Optional[VdbeManager] = VdbeManager.load_from( - load_vdbe_path + load_vdbe_path, + starting_port=9876, ) else: self._vdbe_manager = None diff --git a/src/brad/ui/manager_impl.py b/src/brad/ui/manager_impl.py index 21911583..9e068640 100644 --- a/src/brad/ui/manager_impl.py +++ b/src/brad/ui/manager_impl.py @@ -14,7 +14,6 @@ from brad.blueprint.table import Table from brad.blueprint.manager import BlueprintManager from brad.planner.abstract import BlueprintPlanner -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 @@ -30,6 +29,7 @@ from brad.daemon.front_end_metrics import FrontEndMetric from brad.daemon.system_event_logger import SystemEventLogger, SystemEventRecord from brad.vdbe.manager import VdbeManager +from brad.vdbe.models import VirtualEngine, CreateVirtualEngineArgs logger = logging.getLogger(__name__) @@ -133,8 +133,8 @@ def get_system_state(filter_tables_for_demo: bool = False) -> SystemState: full_routing_policy=blueprint.get_routing_policy(), ) - dbp = DisplayableBlueprint.from_blueprint(blueprint) virtual_infra = manager.vdbe_mgr.infra() + dbp = DisplayableBlueprint.from_blueprint(blueprint, virtual_infra) status = _determine_current_status(manager) if status is Status.Transitioning: next_blueprint = manager.blueprint_mgr.get_transition_metadata().next_blueprint @@ -142,11 +142,14 @@ def get_system_state(filter_tables_for_demo: bool = False) -> SystemState: next_dbp = DisplayableBlueprint.from_blueprint(next_blueprint) else: next_dbp = None + all_tables = [t.name for t in blueprint.tables()] + all_tables.sort() system_state = SystemState( status=status, virtual_infra=virtual_infra, blueprint=dbp, next_blueprint=next_dbp, + all_tables=all_tables, ) return system_state @@ -215,31 +218,50 @@ async def get_predicted_changes(args: PredictedChangesArgs) -> DisplayableBluepr return DisplayableBlueprint.from_blueprint(blueprint) -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.post("/api/1/vdbe") +def create_vdbe(engine: CreateVirtualEngineArgs) -> VirtualEngine: + assert manager is not None + assert manager.vdbe_mgr is not None + + # Do some simple validation. + if engine.name == "": + raise HTTPException(400, "name must be non-empty.") + if engine.max_staleness_ms < 0: + raise HTTPException(400, "max_staleness_ms must be non-negative.") + if engine.p90_latency_slo_ms <= 0: + raise HTTPException(400, "p90_latency_slo_ms must be positive.") + + return manager.vdbe_mgr.add_engine(engine) + + +@app.put("/api/1/vdbe") +def update_vdbe(engine: VirtualEngine) -> VirtualEngine: + assert manager is not None + assert manager.vdbe_mgr is not None + + # Do some simple validation. + if engine.name == "": + raise HTTPException(400, "name must be non-empty.") + if engine.max_staleness_ms < 0: + raise HTTPException(400, "max_staleness_ms must be non-negative.") + if engine.p90_latency_slo_ms <= 0: + raise HTTPException(400, "p90_latency_slo_ms must be positive.") + + try: + return manager.vdbe_mgr.update_engine(engine) + except ValueError as ex: + raise HTTPException(400, str(ex)) from ex + + +@app.delete("/api/1/vdbe/{engine_id}") +def delete_vdbe(engine_id: int) -> None: + assert manager is not None + assert manager.vdbe_mgr is not None + + try: + manager.vdbe_mgr.delete_engine(engine_id) + except ValueError as ex: + raise HTTPException(400, str(ex)) from ex def _determine_current_status(manager_impl: UiManagerImpl) -> Status: diff --git a/src/brad/ui/models.py b/src/brad/ui/models.py index 276a7d2e..ccd2ac19 100644 --- a/src/brad/ui/models.py +++ b/src/brad/ui/models.py @@ -1,5 +1,5 @@ import enum -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Set from pydantic import BaseModel, AwareDatetime from brad.blueprint import Blueprint @@ -18,27 +18,48 @@ class MetricsData(BaseModel): class DisplayableTable(BaseModel): name: str - is_writer: bool = False - mapped_to: List[str] = [] + writable: bool = False class DisplayablePhysicalEngine(BaseModel): name: str + engine: Engine provisioning: Optional[str] tables: List[DisplayableTable] + mapped_vdbes: List[str] class DisplayableBlueprint(BaseModel): engines: List[DisplayablePhysicalEngine] @classmethod - def from_blueprint(cls, blueprint: Blueprint) -> "DisplayableBlueprint": + def from_blueprint( + cls, blueprint: Blueprint, virtual_infra: Optional[VirtualInfrastructure] = None + ) -> "DisplayableBlueprint": + physical_mapping: Dict[Engine, List[str]] = {} + writable: Dict[Engine, Set[str]] = {} + if virtual_infra is not None: + for vdbe in virtual_infra.engines: + try: + physical_mapping[vdbe.mapped_to].append(vdbe.name) + except KeyError: + physical_mapping[vdbe.mapped_to] = [vdbe.name] + + for table in vdbe.tables: + if table.writable: + try: + writable[vdbe.mapped_to].add(table.name) + except KeyError: + writable[vdbe.mapped_to] = {table.name} + engines = [] aurora = blueprint.aurora_provisioning() if aurora.num_nodes() > 0: + writable_aurora = writable.get(Engine.Aurora, set()) aurora_tables = [ - # TODO: Hardcoded Aurora writer. This will change down the road. - DisplayableTable(name=table.name, is_writer=False) + DisplayableTable( + name=table.name, writable=table.name in writable_aurora + ) for table, locations in blueprint.tables_with_locations() if Engine.Aurora in locations ] @@ -46,16 +67,20 @@ def from_blueprint(cls, blueprint: Blueprint) -> "DisplayableBlueprint": engines.append( DisplayablePhysicalEngine( name="Aurora", + engine=Engine.Aurora, provisioning=str(aurora), tables=aurora_tables, + mapped_vdbes=physical_mapping.get(Engine.Aurora, []), ) ) redshift = blueprint.redshift_provisioning() if redshift.num_nodes() > 0: + writable_redshift = writable.get(Engine.Redshift, set()) redshift_tables = [ - # TODO: Hardcoded Redshift writer. This will change down the road. - DisplayableTable(name=table.name, is_writer=False) + DisplayableTable( + name=table.name, writable=table.name in writable_redshift + ) for table, locations in blueprint.tables_with_locations() if Engine.Redshift in locations ] @@ -63,14 +88,16 @@ def from_blueprint(cls, blueprint: Blueprint) -> "DisplayableBlueprint": engines.append( DisplayablePhysicalEngine( name="Redshift", + engine=Engine.Redshift, provisioning=str(redshift), tables=redshift_tables, + mapped_vdbes=physical_mapping.get(Engine.Redshift, []), ) ) + writable_athena = writable.get(Engine.Athena, set()) athena_tables = [ - # TODO: Hardcoded Athena writer. This will change down the road. - DisplayableTable(name=table.name, is_writer=False) + DisplayableTable(name=table.name, writable=table.name in writable_athena) for table, locations in blueprint.tables_with_locations() if Engine.Athena in locations ] @@ -78,7 +105,11 @@ def from_blueprint(cls, blueprint: Blueprint) -> "DisplayableBlueprint": if len(athena_tables) > 0: engines.append( DisplayablePhysicalEngine( - name="Athena", provisioning=None, tables=athena_tables + name="Athena", + engine=Engine.Athena, + provisioning=None, + tables=athena_tables, + mapped_vdbes=physical_mapping.get(Engine.Athena, []), ) ) @@ -96,6 +127,7 @@ class SystemState(BaseModel): virtual_infra: VirtualInfrastructure blueprint: DisplayableBlueprint next_blueprint: Optional[DisplayableBlueprint] + all_tables: List[str] class ClientState(BaseModel): diff --git a/src/brad/vdbe/manager.py b/src/brad/vdbe/manager.py index 7e8cc692..33f5170a 100644 --- a/src/brad/vdbe/manager.py +++ b/src/brad/vdbe/manager.py @@ -1,6 +1,10 @@ import pathlib -from typing import List -from brad.vdbe.models import VirtualInfrastructure, VirtualEngine +from typing import List, Optional +from brad.vdbe.models import ( + VirtualInfrastructure, + VirtualEngine, + CreateVirtualEngineArgs, +) class VdbeManager: @@ -9,16 +13,85 @@ class VdbeManager: """ @classmethod - def load_from(cls, serialized_infra_json: pathlib.Path) -> "VdbeManager": + def load_from( + cls, serialized_infra_json: pathlib.Path, starting_port: int + ) -> "VdbeManager": with open(serialized_infra_json, "r", encoding="utf-8") as f: infra = VirtualInfrastructure.model_validate_json(f.read()) - return cls(infra) + hostname = _get_hostname() + return cls(infra, hostname, starting_port) - def __init__(self, infra: VirtualInfrastructure) -> None: + def __init__( + self, infra: VirtualInfrastructure, hostname: Optional[str], starting_port: int + ) -> None: self._infra = infra + self._hostname = hostname + self._next_port = starting_port + self._next_id = 1 + for engine in self._infra.engines: + self._next_id = max(self._next_id, engine.internal_id) + self._next_id += 1 + + if self._hostname is not None: + for engine in self._infra.engines: + if engine.endpoint is None: + engine.endpoint = f"{self._hostname}:{self._assign_port()}" def infra(self) -> VirtualInfrastructure: return self._infra def engines(self) -> List[VirtualEngine]: return self._infra.engines + + def add_engine(self, create: CreateVirtualEngineArgs) -> VirtualEngine: + engine = VirtualEngine( + internal_id=self._next_id, + name=create.name, + max_staleness_ms=create.max_staleness_ms, + p90_latency_slo_ms=create.p90_latency_slo_ms, + interface=create.interface, + tables=create.tables, + mapped_to=create.mapped_to, + endpoint=None, + ) + self._next_id += 1 + + if self._hostname is not None: + engine.endpoint = f"{self._hostname}:{self._assign_port()}" + + self._infra.engines.append(engine) + return engine + + def update_engine(self, engine: VirtualEngine) -> VirtualEngine: + if engine.endpoint is None and self._hostname is not None: + engine.endpoint = f"{self._hostname}:{self._assign_port()}" + + for i in range(len(self._infra.engines)): + if self._infra.engines[i].internal_id == engine.internal_id: + self._infra.engines[i] = engine + return engine + raise ValueError(f"Engine with id {engine.internal_id} not found") + + def delete_engine(self, engine_id: int) -> None: + for engine in self._infra.engines: + if engine.internal_id == engine_id: + self._infra.engines.remove(engine) + return + raise ValueError(f"Engine with id {engine_id} not found") + + def _assign_port(self) -> int: + port = self._next_port + self._next_port += 1 + return port + + +def _get_hostname() -> str: + import socket + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.settimeout(0) + s.connect(("8.8.8.8", 80)) + return s.getsockname()[0] + finally: + s.close() diff --git a/src/brad/vdbe/models.py b/src/brad/vdbe/models.py index 42f644da..a74c7c0b 100644 --- a/src/brad/vdbe/models.py +++ b/src/brad/vdbe/models.py @@ -1,5 +1,5 @@ import enum -from typing import List +from typing import List, Optional from pydantic import BaseModel from brad.config.engine import Engine @@ -24,15 +24,26 @@ class QueryInterface(enum.Enum): class VirtualEngine(BaseModel): + internal_id: int name: str max_staleness_ms: int p90_latency_slo_ms: int interface: QueryInterface tables: List[VirtualTable] mapped_to: Engine + endpoint: Optional[str] = None class VirtualInfrastructure(BaseModel): schema_name: str engines: List[VirtualEngine] tables: List[SchemaTable] + + +class CreateVirtualEngineArgs(BaseModel): + name: str + max_staleness_ms: int + p90_latency_slo_ms: int + interface: QueryInterface + tables: List[VirtualTable] + mapped_to: Engine diff --git a/tools/serialize_vdbes.py b/tools/serialize_vdbes.py index 363c1aa3..9802dde1 100644 --- a/tools/serialize_vdbes.py +++ b/tools/serialize_vdbes.py @@ -26,6 +26,7 @@ def to_serialize(schema: Dict[str, Any]) -> VirtualInfrastructure: ] a_tables = [VirtualTable(name=name, writable=False) for name in all_table_names] t_engine = VirtualEngine( + internal_id=1, name="VDBE (T)", max_staleness_ms=0, p90_latency_slo_ms=30, @@ -34,6 +35,7 @@ def to_serialize(schema: Dict[str, Any]) -> VirtualInfrastructure: mapped_to=Engine.Aurora, ) a_engine = VirtualEngine( + internal_id=2, name="VDBE (A)", max_staleness_ms=60 * 60 * 1000, # 1 hour p90_latency_slo_ms=30 * 1000, diff --git a/ui/src/App.jsx b/ui/src/App.jsx index bd25ded1..d1192bef 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -14,6 +14,7 @@ function App() { blueprint: null, virtual_infra: null, next_blueprint: null, + all_tables: [], }); const [appState, setAppState] = useState({ previewForm: { @@ -71,6 +72,9 @@ function App() { ...appState, previewForm: { ...previewForm, open: true }, }); + setTimeout(() => { + window.scrollTo({ top: 0, left: 0, behavior: "smooth" }); + }, 0); }; const closePreviewForm = () => { if (!previewForm.open) return; @@ -90,6 +94,9 @@ function App() { const { open } = vdbeForm; if (open) return; setAppState({ ...appState, vdbeForm: { open: true, shownVdbe: vdbe } }); + setTimeout(() => { + window.scrollTo({ top: 0, left: 0, behavior: "smooth" }); + }, 0); }; const closeVdbeForm = () => { const { open } = vdbeForm; @@ -112,6 +119,7 @@ function App() { openVdbeForm={openVdbeForm} closeVdbeForm={closeVdbeForm} setPreviewBlueprint={setPreviewBlueprint} + refreshData={refreshData} /> -

Physical

- {previewBlueprint != null && ( -
- -
- )} + <> +
+ +
- {blueprintToShow && - blueprintToShow.engines && - blueprintToShow.engines.map(({ name, ...props }) => ( - + {blueprint && + blueprint.engines && + blueprint.engines.map(({ name, ...props }) => ( + ))}
+ + ); +} + +function CurrentBlueprint({ blueprint, nextBlueprint }) { + return ( +
+ {blueprint && + blueprint.engines && + blueprint.engines.map(({ engine, mapped_vdbes, ...props }) => ( + + ))} +
+ ); +} + +function BlueprintView({ blueprint, nextBlueprint, previewBlueprint }) { + return ( +
+

Physical

+ {previewBlueprint != null ? ( + + ) : ( + + )}
); } diff --git a/ui/src/components/ConfirmDialog.jsx b/ui/src/components/ConfirmDialog.jsx new file mode 100644 index 00000000..c02bf57b --- /dev/null +++ b/ui/src/components/ConfirmDialog.jsx @@ -0,0 +1,23 @@ +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; + +function ConfirmDialog({ open, onCancel, onConfirm, title, children }) { + return ( + + {title} + + {children} + + + + + + + ); +} + +export default ConfirmDialog; diff --git a/ui/src/components/CreateEditVdbeForm.jsx b/ui/src/components/CreateEditVdbeForm.jsx index 039eff54..beaa825c 100644 --- a/ui/src/components/CreateEditVdbeForm.jsx +++ b/ui/src/components/CreateEditVdbeForm.jsx @@ -20,6 +20,7 @@ import RadioGroup from "@mui/material/RadioGroup"; import FormControlLabel from "@mui/material/FormControlLabel"; import FormLabel from "@mui/material/FormLabel"; import VdbeView from "./VdbeView"; +import { createVdbe, updateVdbe } from "../api"; import "./styles/CreateEditVdbeForm.css"; const ITEM_HEIGHT = 47; @@ -89,34 +90,11 @@ function TableSelector({ selectedTables, setSelectedTables, allTables }) { Click the tables in the preview to toggle their write flags. - - Map VDBE To - - } - label="Aurora" - labelPlacement="end" - /> - } - label="Redshift" - labelPlacement="end" - /> - } - label="Athena" - labelPlacement="end" - /> - - ); } -function CreateEditFormFields({ vdbe, setVdbe, allTables }) { +function CreateEditFormFields({ vdbe, setVdbe, allTables, validEngines }) { const onStalenessChange = (event) => { const maxStalenessMins = parseInt(event.target.value); if (isNaN(maxStalenessMins)) { @@ -142,12 +120,18 @@ function CreateEditFormFields({ vdbe, setVdbe, allTables }) { if (existingTable != null) { nextTables.push(existingTable); } else { - nextTables.push({ name: table, writable: false, mapped_to: null }); + nextTables.push({ name: table, writable: false }); } } setVdbe({ ...vdbe, tables: nextTables }); }; + const onMappedToChange = (event) => { + setVdbe({ ...vdbe, mapped_to: event.target.value }); + }; + + const mappedToEngine = vdbe.mapped_to; + return (
- + + Map VDBE To + + } + label="Aurora" + labelPlacement="end" + checked={mappedToEngine === "aurora"} + onClick={onMappedToChange} + disabled={!validEngines.includes("aurora")} + /> + } + label="Redshift" + labelPlacement="end" + checked={mappedToEngine === "redshift"} + onClick={onMappedToChange} + disabled={!validEngines.includes("redshift")} + /> + } + label="Athena" + labelPlacement="end" + checked={mappedToEngine === "athena"} + onClick={onMappedToChange} + disabled={!validEngines.includes("athena")} + /> + +
); } @@ -224,11 +239,65 @@ function getEmptyVdbe() { }; } -function CreateEditVdbeForm({ currentVdbe, allTables, onCloseClick }) { +function vdbesEqual(vdbe1, vdbe2) { + if (vdbe1 == null || vdbe2 == null) { + return false; + } + if ( + !( + vdbe1.name === vdbe2.name && + vdbe1.max_staleness_ms === vdbe2.max_staleness_ms && + vdbe1.p90_latency_slo_ms === vdbe2.p90_latency_slo_ms && + vdbe1.interface === vdbe2.interface && + vdbe1.tables.length === vdbe2.tables.length && + vdbe1.mapped_to === vdbe2.mapped_to + ) + ) { + return false; + } + + // Check for table equality without regard to order. + return vdbe1.tables.every(({ name, writable }) => { + const matching = vdbe2.tables.find( + (table2) => table2.name === name && table2.writable === writable, + ); + return matching != null; + }); +} + +function isValid(vdbe) { + return ( + vdbe.name != null && + vdbe.max_staleness_ms != null && + vdbe.max_staleness_ms >= 0 && + vdbe.p90_latency_slo_ms != null && + vdbe.p90_latency_slo_ms > 0 && + vdbe.interface != null && + vdbe.tables.length > 0 && + vdbe.mapped_to != null + ); +} + +function validEngines(blueprint) { + if (blueprint == null) { + return []; + } + return blueprint.engines.map((engine) => engine.engine); +} + +function CreateEditVdbeForm({ + currentVdbe, + blueprint, + allTables, + onCloseClick, + onVdbeChangeSuccess, +}) { const isEdit = currentVdbe != null; const [vdbe, setVdbe] = useState( currentVdbe != null ? currentVdbe : getEmptyVdbe(), ); + const [inFlight, setInFlight] = useState(false); + const hasChanges = currentVdbe == null || !vdbesEqual(currentVdbe, vdbe); const onTableClick = (tableName) => { const nextTables = []; @@ -242,6 +311,18 @@ function CreateEditVdbeForm({ currentVdbe, allTables, onCloseClick }) { setVdbe({ ...vdbe, tables: nextTables }); }; + const onSaveClick = async () => { + setInFlight(true); + if (isEdit) { + await updateVdbe(vdbe); + } else { + await createVdbe(vdbe); + } + await onVdbeChangeSuccess(); + setInFlight(false); + onCloseClick(); + }; + return (

@@ -257,24 +338,31 @@ function CreateEditVdbeForm({ currentVdbe, allTables, onCloseClick }) { vdbe={vdbe} setVdbe={setVdbe} allTables={allTables} + validEngines={validEngines(blueprint)} />
-

Preview

+
+ +
- diff --git a/ui/src/components/HighlightContext.jsx b/ui/src/components/HighlightContext.jsx new file mode 100644 index 00000000..473dcf57 --- /dev/null +++ b/ui/src/components/HighlightContext.jsx @@ -0,0 +1,13 @@ +import { createContext } from "react"; + +const HighlightContext = createContext({ + highlight: { + hoveredVdbe: null, + hoveredEngine: null, + }, + setVdbeHighlight: (vdbeName) => {}, + setEngineHighlight: (engine) => {}, + clearHighlight: () => {}, +}); + +export default HighlightContext; diff --git a/ui/src/components/HighlightablePhysDb.jsx b/ui/src/components/HighlightablePhysDb.jsx new file mode 100644 index 00000000..4fd92a66 --- /dev/null +++ b/ui/src/components/HighlightablePhysDb.jsx @@ -0,0 +1,20 @@ +import { useContext } from "react"; +import HighlightContext from "./HighlightContext"; +import PhysDbView from "./PhysDbView"; +import { highlightEngineClass } from "../highlight"; + +function HighlightablePhysDb({ engine, mappedVdbes, ...props }) { + const { highlight, setEngineHighlight, clearHighlight } = + useContext(HighlightContext); + return ( +
setEngineHighlight(engine)} + onMouseLeave={clearHighlight} + > + +
+ ); +} + +export default HighlightablePhysDb; diff --git a/ui/src/components/HighlightableVdbe.jsx b/ui/src/components/HighlightableVdbe.jsx new file mode 100644 index 00000000..a7b46eb5 --- /dev/null +++ b/ui/src/components/HighlightableVdbe.jsx @@ -0,0 +1,20 @@ +import { useContext } from "react"; +import HighlightContext from "./HighlightContext"; +import VdbeView from "./VdbeView"; +import { highlightVdbeClass } from "../highlight"; + +function HighlightableVdbe({ vdbe, ...props }) { + const { highlight, setVdbeHighlight, clearHighlight } = + useContext(HighlightContext); + return ( +
setVdbeHighlight(vdbe.name)} + onMouseLeave={clearHighlight} + > + +
+ ); +} + +export default HighlightableVdbe; diff --git a/ui/src/components/OverallInfraView.jsx b/ui/src/components/OverallInfraView.jsx index 21a181e3..8deead28 100644 --- a/ui/src/components/OverallInfraView.jsx +++ b/ui/src/components/OverallInfraView.jsx @@ -1,10 +1,14 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import VirtualInfraView from "./VirtualInfraView"; import BlueprintView from "./BlueprintView"; import WorkloadInput from "./WorkloadInput"; import CreateEditVdbeForm from "./CreateEditVdbeForm"; import StorageRoundedIcon from "@mui/icons-material/StorageRounded"; +import Snackbar from "@mui/material/Snackbar"; +import HighlightContext from "./HighlightContext"; +import ConfirmDialog from "./ConfirmDialog"; import Panel from "./Panel"; +import { deleteVdbe } from "../api"; function OverallInfraView({ systemState, @@ -13,102 +17,118 @@ function OverallInfraView({ openVdbeForm, closeVdbeForm, setPreviewBlueprint, + refreshData, }) { const { previewForm, vdbeForm } = appState; + const [showVdbeChangeSuccess, setShowVdbeChangeSuccess] = useState(false); const [highlight, setHighlight] = useState({ - hoverEngine: null, - virtualEngines: {}, - physicalEngines: {}, + hoveredVdbe: null, + hoveredEngine: null, }); + const setVdbeHighlight = useCallback((vdbeName) => { + setHighlight({ hoveredVdbe: vdbeName, hoveredEngine: null }); + }, []); + const setEngineHighlight = useCallback((engine) => { + setHighlight({ hoveredVdbe: null, hoveredEngine: engine }); + }, []); + const clearHighlight = useCallback(() => { + setHighlight({ hoveredVdbe: null, hoveredEngine: null }); + }, []); + const highlightContextValue = { + highlight, + setVdbeHighlight, + setEngineHighlight, + clearHighlight, + }; - 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; - } + const onVdbeChangeSuccess = async () => { + await refreshData(); + setShowVdbeChangeSuccess(true); + }; + const handleSnackbarClose = (event, reason) => { + if (reason === "clickaway") { + return; } - setHighlight({ - hoverEngine: engineMarker, - virtualEngines, - physicalEngines, - }); + setShowVdbeChangeSuccess(false); }; - const onTableHoverExit = () => { - setHighlight({ - hoverEngine: null, - virtualEngines: {}, - physicalEngines: {}, - }); + const [deletionState, setDeletionState] = useState({ + showConfirm: false, + deletingVdbe: null, + }); + const openConfirmDelete = (vdbe) => { + setDeletionState({ showConfirm: true, deletingVdbe: vdbe }); + }; + const doDeleteVdbe = async () => { + await deleteVdbe(deletionState.deletingVdbe.internal_id); + await refreshData(); + setDeletionState({ showConfirm: false, deletingVdbe: null }); + setShowVdbeChangeSuccess(true); }; - - const allTables = [ - "tickets", - "theatres", - "movies", - "showings", - "aka_title", - "homes", - "movie_info", - "title", - "company_name", - ]; return ( -
-

- - Data Infrastructure -

-
- - {previewForm.open && ( - ({ name: engine.name, intensity: 1 }), - )} - min={1} - max={10} - onClose={closePreviewForm} - setPreviewBlueprint={setPreviewBlueprint} + +
+

+ + Data Infrastructure +

+
+ + {previewForm.open && ( + ({ name: engine.name, intensity: 1 }), + )} + min={1} + max={10} + onClose={closePreviewForm} + setPreviewBlueprint={setPreviewBlueprint} + /> + )} + {vdbeForm.open && ( + + )} + openVdbeForm(null)} + onEditVdbeClick={openVdbeForm} + onDeleteVdbeClick={openConfirmDelete} + disableVdbeChanges={previewForm.open || vdbeForm.open} /> - )} - {vdbeForm.open && ( - + - )} - openVdbeForm(null)} - onEditVdbeClick={openVdbeForm} - disableVdbeChanges={previewForm.open || vdbeForm.open} - /> -
- - + +
+ + setDeletionState({ showConfirm: false, deletingVdbe: null }) + } + onConfirm={doDeleteVdbe} + > + Are you sure you want to delete the VDBE " + {deletionState.deletingVdbe?.name}"? This action cannot be undone. + +
-
+
); } diff --git a/ui/src/components/PerfView.jsx b/ui/src/components/PerfView.jsx index f5feeadb..3e9a8547 100644 --- a/ui/src/components/PerfView.jsx +++ b/ui/src/components/PerfView.jsx @@ -2,8 +2,8 @@ import { useEffect, useState, useRef, useCallback } from "react"; import { fetchMetrics } from "../api"; import MetricsManager from "../metrics"; import Panel from "./Panel"; -import LatencyPlot from "./LatencyPlot"; import TroubleshootRoundedIcon from "@mui/icons-material/TroubleshootRounded"; +import VdbeMetricsView from "./VdbeMetricsView"; import "./styles/PerfView.css"; const REFRESH_INTERVAL_MS = 30 * 1000; @@ -121,17 +121,6 @@ function PerfView({ virtualInfra, showingPreview }) { const queryLatMetrics = extractMetrics(metricsData, "query_latency_s_p90"); const txnLatMetrics = extractMetrics(metricsData, "txn_latency_s_p90"); - let vdbe1Peak = null; - let vdbe2Peak = null; - if (virtualInfra?.engines != null) { - if (virtualInfra.engines.length > 0) { - vdbe1Peak = virtualInfra.engines[0].p90_latency_slo_ms / 1000; - } - if (virtualInfra.engines.length > 1) { - vdbe2Peak = virtualInfra.engines[1].p90_latency_slo_ms / 1000; - } - } - const columnStyle = { flexGrow: 2, }; @@ -154,28 +143,13 @@ function PerfView({ virtualInfra, showingPreview }) {
-
-

VDBE 1 Query Latency

- -
-
-

VDBE 2 Query Latency

- ( + -
+ ))}
diff --git a/ui/src/components/PhysDbView.jsx b/ui/src/components/PhysDbView.jsx index fad0e73b..6bbb5710 100644 --- a/ui/src/components/PhysDbView.jsx +++ b/ui/src/components/PhysDbView.jsx @@ -2,11 +2,6 @@ import DbCylinder from "./DbCylinder"; import TableView from "./TableView"; import ExpandableTableSet from "./ExpandableTableSet"; import "./styles/PhysDbView.css"; -import { - highlightTableViewClass, - highlightEngineViewClass, - sortTablesToHoist, -} from "../highlight"; function addedTables(tables, nextEngine) { if (nextEngine == null) return []; @@ -22,39 +17,13 @@ function addedTables(tables, nextEngine) { return added; } -function PhysDbView({ - name, - provisioning, - tables, - highlight, - onTableHoverEnter, - onTableHoverExit, - nextEngine, -}) { - const physDbName = name; - const sortedTables = sortTablesToHoist(highlight, physDbName, false, tables); +function PhysDbView({ name, provisioning, tables, nextEngine }) { + const sortedTables = tables; const addedTablesList = addedTables(tables, nextEngine); - const sortedTableComponents = sortedTables.map( - ({ name, writable, mapped_to }) => ( - - onTableHoverEnter(physDbName, name, false, mapped_to) - } - onTableHoverExit={onTableHoverExit} - /> - ), - ); + const sortedTableComponents = sortedTables.map(({ name, writable }) => ( + + )); const addedTableComponents = addedTablesList.map(({ name, writable }) => ( {}} - onTableHoverExit={() => {}} /> )); return ( -
+
{name}
{provisioning}
{nextEngine && ( diff --git a/ui/src/components/TableView.jsx b/ui/src/components/TableView.jsx index 30fac19d..1e728481 100644 --- a/ui/src/components/TableView.jsx +++ b/ui/src/components/TableView.jsx @@ -4,15 +4,7 @@ function WriterMarker({ color }) { return
W
; } -function TableView({ - name, - isWriter, - color, - onTableHoverEnter, - onTableHoverExit, - onTableClick, - highlightClass, -}) { +function TableView({ name, isWriter, color, onTableClick, highlightClass }) { let handleTableClick = onTableClick; if (handleTableClick == null) { handleTableClick = () => {}; @@ -20,8 +12,6 @@ function TableView({ return (
handleTableClick(name)} > {name} diff --git a/ui/src/components/VdbeMetricsView.jsx b/ui/src/components/VdbeMetricsView.jsx new file mode 100644 index 00000000..660b6645 --- /dev/null +++ b/ui/src/components/VdbeMetricsView.jsx @@ -0,0 +1,20 @@ +import LatencyPlot from "./LatencyPlot"; + +function VdbeMetricsView({ vdbe, metrics }) { + const vdbePeak = vdbe.p90_latency_slo_ms / 1000; + return ( +
+

{vdbe.name} VDBE Query Latency

+ +
+ ); +} + +export default VdbeMetricsView; diff --git a/ui/src/components/VdbeView.jsx b/ui/src/components/VdbeView.jsx index b2e6a0d4..91b8c77c 100644 --- a/ui/src/components/VdbeView.jsx +++ b/ui/src/components/VdbeView.jsx @@ -9,11 +9,6 @@ import DeleteRoundedIcon from "@mui/icons-material/DeleteRounded"; import LinkRoundedIcon from "@mui/icons-material/LinkRounded"; import Snackbar from "@mui/material/Snackbar"; import "./styles/VdbeView.css"; -import { - highlightTableViewClass, - highlightEngineViewClass, - sortTablesToHoist, -} from "../highlight"; function formatMilliseconds(milliseconds) { if (milliseconds == null) { @@ -80,7 +75,9 @@ function EditControls({ onEditClick, onDeleteClick }) { function VdbeEndpoint({ endpoint, setShowSnackbar }) { const handleCopy = () => { - navigator.clipboard.writeText(endpoint); + if (navigator.clipboard != null) { + navigator.clipboard.writeText(endpoint); + } setShowSnackbar(true); }; return ( @@ -95,13 +92,11 @@ function VdbeEndpoint({ endpoint, setShowSnackbar }) { function VdbeView({ vdbe, - endpoint, - highlight, - onTableHoverEnter, - onTableHoverExit, onTableClick, editable, onEditClick, + onDeleteClick, + hideEndpoint, }) { if (onEditClick == null) { onEditClick = () => {}; @@ -112,7 +107,8 @@ function VdbeView({ const freshness = formatFreshness(vdbe.max_staleness_ms); const peakLatency = formatMilliseconds(vdbe.p90_latency_slo_ms); const dialect = formatDialect(vdbe.interface); - const sortedTables = sortTablesToHoist(highlight, vengName, true, tables); + const sortedTables = tables; + // const sortedTables = sortTablesToHoist(highlight, vengName, true, tables); const [showSnackbar, setShowSnackbar] = useState(false); const handleClose = (event, reason) => { @@ -123,61 +119,54 @@ function VdbeView({ }; return ( -
-
- {vengName} - {editable && ( - onEditClick(vdbe)} - onDeleteClick={() => {}} + <> +
+
+ {vengName} + {editable && ( + onEditClick(vdbe)} + onDeleteClick={() => onDeleteClick(vdbe)} + /> + )} +
+ {vdbe.endpoint && !hideEndpoint && ( + )} +
+
    +
  • 🌿: {freshness != null ? freshness : "-----"}
  • +
  • + ⏱️:{" "} + {peakLatency != null + ? `p90 Query Latency ≤ ${peakLatency}` + : "-----"} +
  • +
  • 🗣: {dialect != null ? dialect : "-----"}
  • +
+
+ + {sortedTables.map(({ name, writable }) => ( + + ))} +
- {endpoint && ( - - )} -
-
    -
  • 🌿: {freshness != null ? freshness : "-----"}
  • -
  • - ⏱️:{" "} - {peakLatency != null - ? `p90 Query Latency ≤ ${peakLatency}` - : "-----"} -
  • -
  • 🗣: {dialect != null ? dialect : "-----"}
  • -
-
- - {sortedTables.map(({ name, writable, mapped_to }) => ( - - onTableHoverEnter(vengName, name, true, mapped_to) - } - onTableHoverExit={onTableHoverExit} - onTableClick={onTableClick} - /> - ))} - -
+ ); } diff --git a/ui/src/components/VirtualInfraView.jsx b/ui/src/components/VirtualInfraView.jsx index ae9b6a57..9c2cae9b 100644 --- a/ui/src/components/VirtualInfraView.jsx +++ b/ui/src/components/VirtualInfraView.jsx @@ -1,15 +1,14 @@ -import VdbeView from "./VdbeView"; +// import VdbeView from "./VdbeView"; +import HighlightableVdbe from "./HighlightableVdbe"; import AddCircleOutlineRoundedIcon from "@mui/icons-material/AddCircleOutlineRounded"; import Button from "@mui/material/Button"; import "./styles/VirtualInfraView.css"; function VirtualInfraView({ virtualInfra, - highlight, - onTableHoverEnter, - onTableHoverExit, onAddVdbeClick, onEditVdbeClick, + onDeleteVdbeClick, disableVdbeChanges, }) { return ( @@ -17,14 +16,12 @@ function VirtualInfraView({

Virtual

{virtualInfra?.engines?.map((vdbe) => ( - ))}
diff --git a/ui/src/components/styles/CreateEditVdbeForm.css b/ui/src/components/styles/CreateEditVdbeForm.css index f560f8c2..352acb33 100644 --- a/ui/src/components/styles/CreateEditVdbeForm.css +++ b/ui/src/components/styles/CreateEditVdbeForm.css @@ -34,6 +34,12 @@ position: relative; } +.cev-preview-label { + position: absolute; + top: 0; + right: 80px; +} + .cev-preview h2 { position: absolute; top: 0; diff --git a/ui/src/components/styles/PerfView.css b/ui/src/components/styles/PerfView.css index 499d8b60..23eac3c7 100644 --- a/ui/src/components/styles/PerfView.css +++ b/ui/src/components/styles/PerfView.css @@ -1,6 +1,7 @@ .perf-view-wrap { display: flex; flex-direction: column; + gap: 30px 0; } .perf-view-heading { diff --git a/ui/src/components/styles/PhysDbView.css b/ui/src/components/styles/PhysDbView.css index 01d82790..d0b62122 100644 --- a/ui/src/components/styles/PhysDbView.css +++ b/ui/src/components/styles/PhysDbView.css @@ -4,6 +4,7 @@ flex-direction: column; align-items: center; margin-top: 10px; + transition: opacity 0.3s; } .physdb-view-prov { @@ -17,7 +18,6 @@ color: #888; } -.physdb-view.dim .db-cylinder, -.physdb-view.dim .physdb-view-prov { - opacity: 0.3; +.dim .physdb-view { + opacity: 0.5; } diff --git a/ui/src/components/styles/VdbeView.css b/ui/src/components/styles/VdbeView.css index cbad6d78..5a47b05b 100644 --- a/ui/src/components/styles/VdbeView.css +++ b/ui/src/components/styles/VdbeView.css @@ -3,18 +3,16 @@ display: flex; flex-direction: column; align-items: center; - margin-top: 10px; position: relative; + transition: opacity 0.3s; } -.vdbe-view.dim .vdbe-view-props, -.vdbe-view.dim .db-cylinder { - opacity: 0.3; +.dim .vdbe-view { + opacity: 0.5; } .vdbe-view-props { margin: 0 0 15px 0; - transition: opacity 0.3s; } .vdbe-view-props ul { diff --git a/ui/src/components/styles/VirtualInfraView.css b/ui/src/components/styles/VirtualInfraView.css index 7b5f4b98..a33bc896 100644 --- a/ui/src/components/styles/VirtualInfraView.css +++ b/ui/src/components/styles/VirtualInfraView.css @@ -2,6 +2,8 @@ flex-grow: 1; display: flex; justify-content: space-around; + flex-wrap: wrap; + gap: 30px 10px; } .infra-controls { @@ -9,5 +11,5 @@ justify-content: center; align-items: center; width: 100%; - margin: 20px 0 -20px 0; + margin: 40px 0 -30px 0; } diff --git a/ui/src/highlight.js b/ui/src/highlight.js index 98c53b41..1a096881 100644 --- a/ui/src/highlight.js +++ b/ui/src/highlight.js @@ -1,3 +1,31 @@ +function highlightVdbeClass(highlightState, vdbeName, mappedToEngine) { + const { hoveredVdbe, hoveredEngine } = highlightState; + if (hoveredVdbe == null && hoveredEngine == null) { + return ""; + } + + if (hoveredVdbe != null) { + return hoveredVdbe === vdbeName ? "highlight" : "dim"; + } else { + return hoveredEngine === mappedToEngine ? "highlight" : "dim"; + } +} + +function highlightEngineClass(highlightState, engineName, vdbeNames) { + const { hoveredVdbe, hoveredEngine } = highlightState; + if (hoveredVdbe == null && hoveredEngine == null) { + return ""; + } + + if (hoveredEngine != null) { + return hoveredEngine === engineName ? "highlight" : "dim"; + } else { + return vdbeNames.includes(hoveredVdbe) ? "highlight" : "dim"; + } +} + +// The functions below are legacy implementations of the highlight logic. + function highlightTableViewClass( highlightState, engineName, @@ -72,4 +100,12 @@ function sortTablesToHoist(highlightState, currentEngine, isVirtual, tables) { return tableCopy; } -export { highlightTableViewClass, highlightEngineViewClass, sortTablesToHoist }; +export { + highlightVdbeClass, + highlightEngineClass, + + // The functions below are legacy implementations of the highlight logic. + highlightTableViewClass, + highlightEngineViewClass, + sortTablesToHoist, +};