From 935a2444c522bc7c8292b25847d180eb7fefb102 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 19 Jan 2025 23:00:06 +0100 Subject: [PATCH 1/4] fix (web): Animate in async WebWorker, not sync "live & visible" at load --- web/public/demo/greeting3.gexf | 52 ++++++++++++++++++++++++++++++++++ web/src/browser/index.ts | 52 ++++++++++++++-------------------- web/src/browser/util.ts | 25 ++++++++++++++++ 3 files changed, 98 insertions(+), 31 deletions(-) create mode 100644 web/public/demo/greeting3.gexf create mode 100644 web/src/browser/util.ts diff --git a/web/public/demo/greeting3.gexf b/web/public/demo/greeting3.gexf new file mode 100644 index 00000000..3a311809 --- /dev/null +++ b/web/public/demo/greeting3.gexf @@ -0,0 +1,52 @@ + + + + Enola.dev + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/browser/index.ts b/web/src/browser/index.ts index 84c1a83f..866516bf 100644 --- a/web/src/browser/index.ts +++ b/web/src/browser/index.ts @@ -19,26 +19,14 @@ import { DirectedGraph } from "graphology" import { parse } from "graphology-gexf/browser" import { random } from "graphology-layout" -import { - default as forceAtlas2, - ForceAtlas2SynchronousLayoutParameters, - inferSettings, -} from "graphology-layout-forceatlas2" -// TODO import FA2LayoutSupervisor from "graphology-layout-forceatlas2/worker.js" +import { inferSettings } from "graphology-layout-forceatlas2" +import FA2Layout from "graphology-layout-forceatlas2/worker" import { Sigma } from "sigma" - -// TODO Move this to an util.ts file -function getElementByIdOrFail(id: string): HTMLElement { - const element = document.getElementById(id) - if (!element) { - throw new Error(`HTML Element with ID '${id}' not found.`) - } - return element -} +import { getElementByIdOrFail } from "./util" // TODO Handle 404 - display e.g. an "🙅🏽‍♀️" in the DIV container DOM // TODO Replace hard-coded demo with ?q= read from the URL e.g. for "/gexf?q=enola:/inline" -fetch("/demo/picasso.gexf") +fetch("/demo/greeting3.gexf") .then(res => res.text()) .then(gexf => { // Parse GEXF string: @@ -46,22 +34,25 @@ fetch("/demo/picasso.gexf") const graph: DirectedGraph = parse(DirectedGraph, gexf, { addMissingNodes: true }) // https://graphology.github.io/standard-library/layout-forceatlas2.html: - // "Each node’s starting position must be set before running ForceAtlas 2 layout" + // "Each node’s starting position must be set before running ForceAtlas 2 layout." + // https://www.npmjs.com/package/graphology-layout-forceatlas2#pre-requisites: + // "(...) edge-case where the layout cannot be computed if all of your nodes starts with x=0 and y=0." random.assign(graph) // Configure ForceAtlas2 layout settings // TODO Review and adjust the default settings... - // TODO Adjust iterations for desired layout quality/performance... // https://graphology.github.io/standard-library/layout-forceatlas2.html#settings - const fa2Settings = inferSettings(graph) as ForceAtlas2SynchronousLayoutParameters - fa2Settings.iterations = 500 - // TODO const layout = new FA2LayoutSupervisor(graph, fa2Settings) - if (fa2Settings.settings) { - fa2Settings.settings.gravity = 1 - // ? fa2Settings.settings.scalingRatio = 2 - // ? fa2Settings.settings.strongGravityMode = false - } - forceAtlas2.assign(graph, fa2Settings) + const fa2Settings = inferSettings(graph) + fa2Settings.adjustSizes = true // TODO Add Node Sizes to Graph + // NOT, because unstable: fa2Settings.barnesHutOptimize = true + fa2Settings.gravity = 1 + fa2Settings.scalingRatio = 1 + fa2Settings.strongGravityMode = true + + const fa2Layout = new FA2Layout(graph, { settings: fa2Settings }) + fa2Layout.start() + // TODO UI Buttons to stop() and re-start() the layout + // TODO layout.kill() when element is removed from DOM - but how do we know when to do that?! // Retrieve some useful DOM elements const container = getElementByIdOrFail("container") @@ -88,12 +79,11 @@ fetch("/demo/picasso.gexf") void camera.animatedReset({ duration: 600 }) }) - // Bind labels threshold to range input + const initialLabelsThreshold = 0 + labelsThresholdRange.value = initialLabelsThreshold.toString() + renderer.setSetting("labelRenderedSizeThreshold", initialLabelsThreshold) labelsThresholdRange.addEventListener("input", () => { renderer.setSetting("labelRenderedSizeThreshold", +labelsThresholdRange.value) }) - - // Set proper range initial value: - labelsThresholdRange.value = renderer.getSetting("labelRenderedSizeThreshold").toString() }) .catch((error: Error) => console.error(error)) diff --git a/web/src/browser/util.ts b/web/src/browser/util.ts new file mode 100644 index 00000000..37379863 --- /dev/null +++ b/web/src/browser/util.ts @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2025 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function getElementByIdOrFail(id: string): HTMLElement { + const element = document.getElementById(id) + if (!element) { + throw new Error(`HTML Element with ID '${id}' not found.`) + } + return element +} From 8ec1a38bcf1d0a21e70f426db54d99a107928e05 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 19 Jan 2025 23:58:09 +0100 Subject: [PATCH 2/4] fix (web): Externalize CSS into separate file, and test it's bundled (it is) --- web/bun.lock | 3 +++ web/package.json | 1 + web/public/enola.css | 9 +++++++++ web/public/index.html | 13 ++++++++----- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 web/public/enola.css diff --git a/web/bun.lock b/web/bun.lock index e8a098d5..b636d1dc 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -17,6 +17,7 @@ "@types/jest": "^29.5.14", "@types/sigmajs": "^1.0.32", "graphology-types": "^0.24.8", + "prettier": "^3.4.2", "typescript": "^5.7.3", }, }, @@ -328,6 +329,8 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + "prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="], + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], diff --git a/web/package.json b/web/package.json index 8f08d694..3ae4cbb2 100644 --- a/web/package.json +++ b/web/package.json @@ -25,6 +25,7 @@ "@types/jest": "^29.5.14", "@types/sigmajs": "^1.0.32", "graphology-types": "^0.24.8", + "prettier": "^3.4.2", "typescript": "^5.7.3" } } diff --git a/web/public/enola.css b/web/public/enola.css new file mode 100644 index 00000000..dd0c0f14 --- /dev/null +++ b/web/public/enola.css @@ -0,0 +1,9 @@ +body { + background: lightgrey; +} + +#container { + width: 100%; + height: 600px; /* TODO How to 100% ? */ + background: white; +} diff --git a/web/public/index.html b/web/public/index.html index 9a8740ef..d5e737ca 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -20,19 +20,22 @@ Network + - +
-
-
-
+
+
+
+ +
-
+
From e42a6a7271c039a6058e68fea612119d4e990ec3 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Mon, 20 Jan 2025 00:42:47 +0100 Subject: [PATCH 3/4] fix (web): CSS is a PITA, but at least it already looks much better now like this --- web/ToDo.md | 11 ++++------- web/public/enola.css | 40 +++++++++++++++++++++++++++++++++++++++- web/public/index.html | 22 ++++++++++++---------- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/web/ToDo.md b/web/ToDo.md index 8da07bc3..c11a75f1 100644 --- a/web/ToDo.md +++ b/web/ToDo.md @@ -24,10 +24,10 @@ OR make `../enola server` send either (better) `Cache-Control: no-cache` & `ETag: "abcdef1234"` (or just `Cache-Control: no-store`; or `max-age=0`, really same?) 1. Fix CORS ? +1. Is it OK to have a `package.json` e.g. for the Prettier version at the root, and have another one in `web/package.json`? ## Functional -* Play more with https://graphology.github.io/standard-library/layout-forceatlas2.html#settings * Try out https://graphology.github.io/standard-library/layout-noverlap * Do coloring using https://graphology.github.io/standard-library/communities-louvain; see https://gemini.google.com/app/4e3c639fc5213673 @@ -40,14 +40,11 @@ * Highlight when hovering over label as well, not just dot * Let users drag nodes around * Hover over Nodes should highlight all its edges -* Click on node should open Enola details page on the right -* Dark Modus support -## Visual +* Merge with //java/dev/enola/web/resources/static/main.css and show green NavBar +* Click on node should open Enola details page on the right -* Introduce a CSS so that controls are in a single row, with a nicer font -* `
` should fill entire available space -* There shouldn't be any space around the graph +* Dark Modus support! ;-) ## Technical diff --git a/web/public/enola.css b/web/public/enola.css index dd0c0f14..9ff184ba 100644 --- a/web/public/enola.css +++ b/web/public/enola.css @@ -1,9 +1,47 @@ body { background: lightgrey; + height: 100vh; + margin: 0; } #container { width: 100%; - height: 600px; /* TODO How to 100% ? */ + height: 100vh; background: white; + z-index: 0; +} + +#buttons { + position: absolute; + right: 1em; + top: 2em; + display: flex; + z-index: 1; +} +button, +.button { + display: inline-flex; + align-items: center; + padding: 0.3em 0.3em; + margin: 0 0.5em; + background-color: #f0f0f0; + border: 1px solid #ccc; + border-radius: 4px; + cursor: pointer; +} +.button label { + margin-right: 0.5em; +} +.button input[type="range"] { + width: 7em; +} +button:hover, +.button:hover { + background-color: #e0e0e0; +} +#buttons > button img { + height: 2em; +} +#buttons > button:last-child { + margin-right: 0; } diff --git a/web/public/index.html b/web/public/index.html index d5e737ca..f9af83e0 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -24,18 +24,20 @@ -
-
-
-
- -
-
- - +
+
+
+ + +
+ + + +
+ +
-
From 53e5852d4fb770b66ff39034ab93cfacca2af661 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Mon, 20 Jan 2025 00:48:26 +0100 Subject: [PATCH 4/4] fix (build): Only create Git Version on CI, to (hopefully) fix the real root cause of too frequent Bazel rebuilds --- tools/version/version.bash | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/version/version.bash b/tools/version/version.bash index 7ffaee1f..2b399ebe 100755 --- a/tools/version/version.bash +++ b/tools/version/version.bash @@ -19,9 +19,15 @@ set -euo pipefail # Inspired e.g. by https://github.com/palantir/gradle-git-version +# This is a PITA (!) during development, because every change and commit on //web/ +# triggers a full rebuild of //java/. We therefore now only run this on CI: +if [ -z "${CI:-""}" ]; then + exit 0 +fi + # It's *VERY* important that this script *ONLY* touches the tools/version/VERSION file -# when its content actually changed. This is otherwise it triggers a frequent full rebuild. -# This is because Bazel (also?) looks at the timestamp of the file to determine if it needs +# when its content actually changed. Otherwise it triggers a frequent full rebuild. This is +# because Bazel (also?) looks at the timestamp of the file to determine if it needs # to rebuild, not ([only?] a hash of) its content. NEW_VERSION=$(git describe --tags --always --first-parent)