diff --git a/dashboard/components/FragmentGraph.tsx b/dashboard/components/FragmentGraph.tsx
index 6ed35c536b47e..d9ca56fea69e8 100644
--- a/dashboard/components/FragmentGraph.tsx
+++ b/dashboard/components/FragmentGraph.tsx
@@ -265,7 +265,7 @@ export default function FragmentGraph({
.append("path")
.attr("fill", "none")
.attr("stroke", theme.colors.gray[700])
- .attr("stroke-width", 1.5) // TODO
+ .attr("stroke-width", 1.5)
.call(applyEdge)
return sel
}
diff --git a/dashboard/components/StatusLamp.js b/dashboard/components/StatusLamp.js
deleted file mode 100644
index 6cadd9e2e7764..0000000000000
--- a/dashboard/components/StatusLamp.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2024 RisingWave Labs
- *
- * 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
- *
- * http://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 default function StatusLamp(props) {
- return (
-
- )
-}
diff --git a/dashboard/lib/color.js b/dashboard/lib/color.js
deleted file mode 100644
index 8b6d7d0fccd6a..0000000000000
--- a/dashboard/lib/color.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright 2024 RisingWave Labs
- *
- * 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
- *
- * http://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.
- *
- */
-const two = [
- ["#6FE0D3", "#E09370"],
- ["#75D0E0", "#E0A575"],
- ["#75BAE0", "#E0B175"],
- ["#77A5E0", "#E0BC77"],
- ["#768CE0", "#E0C577"],
- ["#7575E0", "#E0CD75"],
- ["#A978E0", "#E0DF79"],
- ["#C977E0", "#B9E077"],
- ["#E072D7", "#92E072"],
- ["#E069A4", "#69E069"],
- ["#E06469", "#65E086"],
- ["#E07860", "#60E0B2"],
- ["#E08159", "#5AE0CE"],
- ["#E09C5C", "#5CC5E0"],
- ["#E0B763", "#6395E0"],
- ["#E0CE5A", "#6B5AE0"],
- ["#C8E051", "#AA51E0"],
- ["#92E06F", "#E070DB"],
- ["#79E085", "#E07998"],
- ["#80E0B1", "#E08C80"],
- ["#91DBE0", "#E0B292"],
-]
-
-const twoGradient = [
- ["#1976d2", "#a6c9ff"],
- ["#FFF38A", "#E0D463"],
- ["#A3ACFF", "#7983DF"],
- ["#A6C9FF", "#7BA3DF"],
- ["#FFBE8C", "#E09B65"],
- ["#FFD885", "#E0B65D"],
- ["#9EE2FF", "#73BEDF"],
- ["#DAFF8F", "#B8E066"],
- ["#FFC885", "#E0A65D"],
- ["#9EFCFF", "#74DCDF"],
- ["#FBFF8C", "#DBE065"],
- ["#9CFFDE", "#71DFBB"],
- ["#FFAF91", "#E08869"],
- ["#B699FF", "#9071E0"],
- ["#9EFFB6", "#74DF8F"],
- ["#FFA19C", "#E07872"],
- ["#AEFF9C", "#85DF71"],
- ["#FF96B9", "#E06D94"],
- ["#FFE785", "#E0C75F"],
- ["#FF94FB", "#E06BDC"],
- ["#DA99FF", "#B66FE0"],
- ["#8F93FF", "#666AE0"],
-]
-
-const five = [
- ["#A8936C", "#F5D190", "#8B84F5", "#9AA84A", "#E1F578"],
- ["#A87C6A", "#F5AB8E", "#82CBF5", "#A89348", "#F5D876"],
- ["#A87490", "#F59DCB", "#90F5C7", "#A87752", "#F5B584"],
- ["#856FA8", "#B995F5", "#BAF58A", "#A84D5B", "#F57D8E"],
- ["#7783A8", "#A2B4F5", "#F5EE95", "#9C56A8", "#E589F5"],
- ["#74A895", "#9DF5D4", "#F5BF91", "#526CA8", "#84A6F5"],
- ["#74A878", "#9DF5A3", "#F5A290", "#5298A8", "#84DFF5"],
- ["#94A877", "#D2F5A2", "#F596B6", "#56A88C", "#89F5D0"],
- ["#A8A072", "#F5E79A", "#CD8DF5", "#5DA851", "#92F582"],
- ["#A89176", "#F5CD9F", "#92A3F5", "#A8A554", "#F5F087"],
- ["#A8726A", "#F59B8E", "#83ECF5", "#A88948", "#F5CB76"],
-]
-export function TwoColor(index) {
- return two[index % two.length]
-}
-
-export function FiveColor(index) {
- return five[index % five.length]
-}
-
-let s = Math.random() * 100
-export function TwoGradient(index) {
- return twoGradient[(Math.round(s) + index) % two.length]
-}
diff --git a/dashboard/lib/graaphEngine/canvasEngine.js b/dashboard/lib/graaphEngine/canvasEngine.js
deleted file mode 100644
index df661b53c5ec6..0000000000000
--- a/dashboard/lib/graaphEngine/canvasEngine.js
+++ /dev/null
@@ -1,598 +0,0 @@
-/*
- * Copyright 2024 RisingWave Labs
- *
- * 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
- *
- * http://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.
- *
- */
-import { fabric } from "fabric"
-
-// Disable cache to improve performance.
-fabric.Object.prototype.objectCaching = false
-fabric.Object.prototype.statefullCache = false
-fabric.Object.prototype.noScaleCache = true
-fabric.Object.prototype.needsItsOwnCache = () => false
-
-export class DrawElement {
- /**
- * @param {{svgElement: d3.Selection}} props
- */
- constructor(props) {
- /**
- * @type {{svgElement: d3.Selection}}
- */
- this.props = props
- if (props.canvasElement) {
- props.engine.canvas.add(props.canvasElement)
- props.canvasElement.on("mouse:down", (e) => {
- console.log(e)
- })
- }
-
- this.eventHandler = new Map()
- }
-
- // TODO: this method is for migrating from d3.js to fabric.js.
- // This should be replaced by a more suitable way.
- _attrMap(key, value) {
- return [key, value]
- }
-
- // TODO: this method is for migrating from d3.js to fabric.js.
- // This should be replaced by a more suitable way.
- attr(key, value) {
- let setting = this._attrMap(key, value)
- if (setting && setting.length === 2) {
- this.props.canvasElement &&
- this.props.canvasElement.set(setting[0], setting[1])
- }
- return this
- }
-
- _afterPosition() {
- let ele = this.props.canvasElement
- ele && this.props.engine._addDrawElement(this)
- }
-
- // TODO: this method is for migrating from d3.js to fabric.js.
- // This should be replaced by a more suitable way.
- position(x, y) {
- this.props.canvasElement.set("left", x)
- this.props.canvasElement.set("top", y)
- this._afterPosition()
- return this
- }
-
- on(event, callback) {
- this.eventHandler.set(event, callback)
- return this
- }
-
- getEventHandler(event) {
- return this.eventHandler.get(event)
- }
-
- // TODO: this method is for migrating from d3.js to fabric.js.
- // This should be replaced by a more suitable way.
- style(key, value) {
- return this.attr(key, value)
- }
-
- classed(clazz, flag) {
- this.props.engine.classedElement(clazz, this, flag)
- return this
- }
-}
-
-export class Group extends DrawElement {
- /**
- * @param {{engine: CanvasEngine}} props
- */
- constructor(props) {
- super(props)
-
- this.appendFunc = {
- g: this._appendGroup,
- circle: this._appendCircle,
- rect: this._appendRect,
- text: this._appendText,
- path: this._appendPath,
- polygon: this._appendPolygan,
- }
-
- this.basicSetting = {
- engine: props.engine,
- }
- }
-
- _appendGroup = () => {
- return new Group(this.basicSetting)
- }
-
- _appendCircle = () => {
- return new Circle({
- ...this.basicSetting,
- ...{
- canvasElement: new fabric.Circle({
- selectable: false,
- hoverCursor: "pointer",
- }),
- },
- })
- }
-
- _appendRect = () => {
- return new Rectangle({
- ...this.basicSetting,
- ...{
- canvasElement: new fabric.Rect({
- selectable: false,
- hoverCursor: "pointer",
- }),
- },
- })
- }
-
- _appendText = () => {
- return (content) =>
- new Text({
- ...this.basicSetting,
- ...{
- canvasElement: new fabric.Text(content || "undefined", {
- selectable: false,
- textAlign: "justify-center",
- }),
- },
- })
- }
-
- _appendPath = () => {
- return (d) =>
- new Path({
- ...this.basicSetting,
- ...{
- canvasElement: new fabric.Path(d, { selectable: false }),
- },
- })
- }
-
- _appendPolygan = () => {
- return new Polygan(this.basicSetting)
- }
-
- append = (type) => {
- return this.appendFunc[type]()
- }
-}
-
-export class Rectangle extends DrawElement {
- /**
- * @param {{g: fabric.Group}} props
- */
- constructor(props) {
- super(props)
- this.props = props
- }
-
- init(x, y, width, height) {
- let ele = this.props.canvasElement
- ele.set("left", x)
- ele.set("top", y)
- ele.set("width", width)
- ele.set("height", height)
- super._afterPosition()
- return this
- }
-
- _attrMap(key, value) {
- if (key === "rx") {
- this.props.canvasElement.set("rx", value)
- this.props.canvasElement.set("ry", value)
- return false
- }
- return [key, value]
- }
-}
-
-export class Circle extends DrawElement {
- /**
- * @param {{svgElement: d3.Selection}} props
- */
- constructor(props) {
- super(props)
- this.props = props
- this.radius = 0
- }
-
- init(x, y, r) {
- this.props.canvasElement.set("left", x - r)
- this.props.canvasElement.set("top", y - r)
- this.props.canvasElement.set("radius", r)
- super._afterPosition()
- return this
- }
-
- _attrMap(key, value) {
- if (key === "r") {
- this.radius = value
- return ["radius", value]
- }
- if (key === "cx") {
- return ["left", value - this.radius]
- }
- if (key === "cy") {
- return ["top", value - this.radius]
- }
- return [key, value]
- }
-}
-
-export class Text extends DrawElement {
- /**
- * @param {{svgElement: d3.Selection, any, null, undefined>}} props
- */
- constructor(props) {
- super(props)
- this.props = props
- }
-
- position(x, y) {
- let e = this.props.canvasElement
- e.set("top", y)
- e.set("left", x)
- super._afterPosition()
- return this
- }
-
- _attrMap(key, value) {
- if (key === "text-anchor") {
- return ["textAlign", value]
- }
- if (key === "font-size") {
- return ["fontSize", value]
- }
- return [key, value]
- }
-
- text(content) {
- return this
- }
-
- getWidth() {}
-}
-
-export class Polygan extends DrawElement {
- constructor(props) {
- super(props)
- this.props = props
- }
-}
-
-export class Path extends DrawElement {
- constructor(props) {
- super(props)
- this.props = props
- this.strokeWidth = 1
- super._afterPosition()
- }
-
- _attrMap(key, value) {
- if (key === "fill") {
- return ["fill", value === "none" ? false : value]
- }
- if (key === "stroke-width") {
- this.props.canvasElement.set(
- "top",
- this.props.canvasElement.get("top") - value / 2
- )
- return ["strokeWidth", value]
- }
- if (key === "stroke-dasharray") {
- return ["strokeDashArray", value.split(",")]
- }
- if (key === "layer") {
- if (value === "back") {
- this.props.canvasElement.canvas.sendToBack(this.props.canvasElement)
- }
- return false
- }
- return [key, value]
- }
-}
-
-// TODO: Use rbtree
-class CordMapper {
- constructor() {
- this.map = new Map()
- }
-
- rangeQuery(start, end) {
- let rtn = new Set()
- for (let [k, s] of this.map.entries()) {
- if (start <= k && k <= end) {
- s.forEach((v) => rtn.add(v))
- }
- }
- return rtn
- }
-
- insert(k, v) {
- if (this.map.has(k)) {
- this.map.get(k).add(v)
- } else {
- this.map.set(k, new Set([v]))
- }
- }
-}
-
-class GridMapper {
- constructor() {
- this.xMap = new CordMapper()
- this.yMap = new CordMapper()
- this.gs = 100 // grid size
- }
-
- _getKey(value) {
- return Math.round(value / this.gs)
- }
-
- addObject(minX, maxX, minY, maxY, ele) {
- for (let i = minX; i <= maxX + this.gs; i += this.gs) {
- this.xMap.insert(this._getKey(i), ele)
- }
- for (let i = minY; i <= maxY + this.gs; i += this.gs) {
- this.yMap.insert(this._getKey(i), ele)
- }
- }
-
- areaQuery(minX, maxX, minY, maxY) {
- let xs = this.xMap.rangeQuery(this._getKey(minX), this._getKey(maxX))
- let ys = this.yMap.rangeQuery(this._getKey(minY), this._getKey(maxY))
- let rtn = new Set()
- xs.forEach((e) => {
- if (ys.has(e)) {
- rtn.add(e)
- }
- })
- return rtn
- }
-}
-
-export class CanvasEngine {
- /**
- * @param {string} canvasId The DOM id of the canvas
- * @param {number} height the height of the canvas
- * @param {number} width the width of the canvas
- */
- constructor(canvasId, height, width) {
- let canvas = new fabric.Canvas(canvasId)
- canvas.selection = false // improve performance
-
- this.height = height
- this.width = width
- this.canvas = canvas
- this.clazzMap = new Map()
- this.topGroup = new Group({ engine: this })
- this.gridMapper = new GridMapper()
- this.canvasElementToDrawElement = new Map()
-
- let that = this
- canvas.on("mouse:wheel", function (opt) {
- var evt = opt.e
- if (evt.ctrlKey === true) {
- var delta = opt.e.deltaY
- var zoom = canvas.getZoom()
- zoom *= 0.999 ** delta
- if (zoom > 10) zoom = 10
- if (zoom < 0.03) zoom = 0.03
- canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom)
- that._refreshView()
- evt.preventDefault()
- evt.stopPropagation()
- } else {
- that.moveCamera(-evt.deltaX, -evt.deltaY)
- evt.preventDefault()
- evt.stopPropagation()
- }
- })
-
- canvas.on("mouse:down", function (opt) {
- var evt = opt.e
- this.isDragging = true
- this.selection = false
- this.lastPosX = evt.clientX
- this.lastPosY = evt.clientY
-
- that._handleClickEvent(opt.target)
- })
-
- canvas.on("mouse:move", function (opt) {
- if (this.isDragging) {
- var e = opt.e
- that.moveCamera(e.clientX - this.lastPosX, e.clientY - this.lastPosY)
- this.lastPosX = e.clientX
- this.lastPosY = e.clientY
- }
- })
- canvas.on("mouse:up", function (opt) {
- this.setViewportTransform(this.viewportTransform)
- this.isDragging = false
- this.selection = true
- })
- }
-
- /**
- * Move the current view point.
- * @param {number} deltaX
- * @param {number} deltaY
- */
- async moveCamera(deltaX, deltaY) {
- this.canvas.setZoom(this.canvas.getZoom()) // essential for rendering (seems like a bug)
- let vpt = this.canvas.viewportTransform
- vpt[4] += deltaX
- vpt[5] += deltaY
- this._refreshView()
- }
-
- /**
- * Invoke the click handler of an object.
- * @param {fabric.Object} target
- */
- async _handleClickEvent(target) {
- if (target === null) {
- return
- }
- let ele = this.canvasElementToDrawElement.get(target)
- let func = ele.getEventHandler("click")
- if (func) {
- func()
- }
- }
-
- /**
- * Set the objects in the current view point visible.
- * And set other objects not visible.
- */
- async _refreshView() {
- const padding = 50 // Make the rendering area a little larger.
- let vpt = this.canvas.viewportTransform
- let zoom = this.canvas.getZoom()
- let cameraWidth = this.width
- let cameraHeight = this.height
- let minX = -vpt[4] - padding
- let maxX = -vpt[4] + cameraWidth + padding
- let minY = -vpt[5] - padding
- let maxY = -vpt[5] + cameraHeight + padding
- let visibleSet = this.gridMapper.areaQuery(
- minX / zoom,
- maxX / zoom,
- minY / zoom,
- maxY / zoom
- )
-
- this.canvas.getObjects().forEach((e) => {
- if (visibleSet.has(e)) {
- e.visible = true
- } else {
- e.visible = false
- }
- })
-
- this.canvas.requestRenderAll()
- }
-
- /**
- * Register an element to the engine. This should
- * be called when a DrawElement instance is added
- * to the canvas.
- * @param {DrawElement} ele
- */
- _addDrawElement(ele) {
- let canvasElement = ele.props.canvasElement
- this.canvasElementToDrawElement.set(canvasElement, ele)
- this.gridMapper.addObject(
- canvasElement.left,
- canvasElement.left + canvasElement.width,
- canvasElement.top,
- canvasElement.top + canvasElement.height,
- canvasElement
- )
- }
-
- /**
- * Assign a class to an object or remove a class from it.
- * @param {string} clazz class name
- * @param {DrawElement} element target object
- * @param {boolean} flag true if the object is assigned, otherwise
- * remove the class from the object
- */
- classedElement(clazz, element, flag) {
- if (!flag) {
- this.clazzMap.has(clazz) && this.clazzMap.get(clazz).delete(element)
- } else {
- if (this.clazzMap.has(clazz)) {
- this.clazzMap.get(clazz).add(element)
- } else {
- this.clazzMap.set(clazz, new Set([element]))
- }
- }
- }
-
- /**
- * Move current view point to the object specified by
- * the selector. The selector is the class of the
- * target object for now.
- * @param {string} selector The class of the target object
- */
- locateTo(selector) {
- //
- let selectorSet = this.clazzMap.get(selector)
- if (selectorSet) {
- let arr = Array.from(selectorSet)
- if (arr.length > 0) {
- let ele = arr[0]
- let x = ele.props.canvasElement.get("left")
- let y = ele.props.canvasElement.get("top")
- let scale = 0.6
- this.canvas.setZoom(scale)
- let vpt = this.canvas.viewportTransform
- vpt[4] = (-x + this.width * 0.5) * scale
- vpt[5] = (-y + this.height * 0.5) * scale
- this.canvas.requestRenderAll()
- this._refreshView()
- }
- }
- }
-
- /**
- * Move current view point to (0, 0)
- */
- resetCamera() {
- let zoom = this.canvas.getZoom()
- zoom *= 0.999
- this.canvas.setZoom(zoom)
- let vpt = this.canvas.viewportTransform
- vpt[4] = 0
- vpt[5] = 0
- this.canvas.requestRenderAll()
- this._refreshView()
- }
-
- /**
- * Dispose the current canvas. Remove all the objects to
- * free memory. All objects in the canvas will be removed.
- */
- cleanGraph() {
- console.log("clean called")
- this.canvas.dispose()
- }
-
- /**
- * Resize the canvas. This is called when the browser size
- * is changed, such that the canvas can fix the current
- * size of the browser.
- *
- * Note that the outer div box of the canvas will be set
- * according to the parameters. However, the width and
- * height of the canvas is double times of the parameters.
- * This is the feature of fabric.js to keep the canvas
- * in high resolution all the time.
- *
- * @param {number} width the width of the canvas
- * @param {number} height the height of the canvas
- */
- resize(width, height) {
- this.width = width
- this.height = height
- this.canvas.setDimensions({ width: this.width, height: this.height })
- }
-}
diff --git a/dashboard/lib/graaphEngine/svgEngine.js b/dashboard/lib/graaphEngine/svgEngine.js
deleted file mode 100644
index 8102b79df0a4b..0000000000000
--- a/dashboard/lib/graaphEngine/svgEngine.js
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright 2024 RisingWave Labs
- *
- * 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
- *
- * http://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.
- *
- */
-import * as d3 from "d3"
-
-export class DrawElement {
- /**
- * @param {{svgElement: d3.Selection}} props
- */
- constructor(props) {
- /**
- * @type {{svgElement: d3.Selection}}
- */
- this.props = props
- this.appendFunc = {
- g: this._appendGroup,
- circle: this._appendCircle,
- rect: this._appendRect,
- text: this._appendText,
- path: this._appendPath,
- polygon: this._appendPolygan,
- }
- }
-
- _appendGroup = () => {
- return new Group({ svgElement: this.props.svgElement.append("g") })
- }
-
- _appendCircle = () => {
- return new Circle({ svgElement: this.props.svgElement.append("circle") })
- }
-
- _appendRect = () => {
- return new Rectangle({ svgElement: this.props.svgElement.append("rect") })
- }
-
- _appendText = () => {
- return new Text({ svgElement: this.props.svgElement.append("text") })
- }
-
- _appendPath = () => {
- return new Path({ svgElement: this.props.svgElement.append("path") })
- }
-
- _appendPolygan = () => {
- return new Polygan({ svgElement: this.props.svgElement.append("polygon") })
- }
-
- on = (event, callback) => {
- this.props.svgElement.on(event, callback)
- return this
- }
-
- style = (key, value) => {
- this.props.svgElement.style(key, value)
- return this
- }
-
- classed = (clazz, flag) => {
- this.props.svgElement.classed(clazz, flag)
- return this
- }
-
- attr = (key, value) => {
- this.props.svgElement.attr(key, value)
- return this
- }
-
- append = (type) => {
- return this.appendFunc[type]()
- }
-}
-
-export class Group extends DrawElement {
- /**
- * @param {{svgElement: d3.Selection}} props
- */
- constructor(props) {
- super(props)
- }
-}
-
-export class Rectangle extends DrawElement {
- /**
- * @param {{svgElement: d3.Selection}} props
- */
- constructor(props) {
- super(props)
- }
-}
-
-export class Circle extends DrawElement {
- /**
- * @param {{svgElement: d3.Selection}} props
- */
- constructor(props) {
- super(props)
- }
-}
-
-export class Text extends DrawElement {
- /**
- * @param {{svgElement: d3.Selection, any, null, undefined>}} props
- */
- constructor(props) {
- super(props)
- this.props = props
- }
-
- text(content) {
- this.props.svgElement.text(content)
- return this
- }
-
- getWidth() {
- return this.props.svgElement.node().getComputedTextLength()
- }
-}
-
-export class Polygan extends DrawElement {
- constructor(props) {
- super(props)
- this.props = props
- }
-}
-
-export class Path extends DrawElement {
- constructor(props) {
- super(props)
- }
-}
-
-const originalZoom = new d3.ZoomTransform(0.5, 0, 0)
-
-export class SvgEngine {
- /**
- * @param {{g: d3.Selection}} props
- */
- constructor(svgRef, height, width) {
- this.height = height
- this.width = width
- this.svgRef = svgRef
-
- d3.select(svgRef).selectAll("*").remove()
- this.svg = d3.select(svgRef).attr("viewBox", [0, 0, width, height])
-
- this._g = this.svg.append("g").attr("class", "top")
- this.topGroup = new Group({ svgElement: this._g })
-
- this.transform
- this.zoom = d3.zoom().on("zoom", (e) => {
- this.transform = e.transform
- this._g.attr("transform", e.transform)
- })
-
- this.svg.call(this.zoom).call(this.zoom.transform, originalZoom)
- this.svg.on("pointermove", (event) => {
- this.transform.invert(d3.pointer(event))
- })
- }
-
- locateTo(selector) {
- let selection = d3.select(selector)
- if (!selection.empty()) {
- this.svg
- .call(this.zoom)
- .call(
- this.zoom.transform,
- new d3.ZoomTransform(
- 0.7,
- -0.7 * selection.attr("x"),
- 0.7 * (-selection.attr("y") + this.height / 2)
- )
- )
- }
- }
-
- resetCamera() {
- this.svg.call(this.zoom.transform, originalZoom)
- }
-
- cleanGraph() {
- d3.select(this.svgRef).selectAll("*").remove()
- }
-}
diff --git a/dashboard/lib/str.js b/dashboard/lib/str.js
deleted file mode 100644
index 52af7892f4a75..0000000000000
--- a/dashboard/lib/str.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright 2024 RisingWave Labs
- *
- * 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
- *
- * http://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 capitalize(sentence) {
- let words = sentence.split(" ")
- let s = ""
- for (let word of words) {
- s += word.charAt(0).toUpperCase() + word.slice(1, word.length)
- }
- return s
-}
diff --git a/dashboard/lib/streamPlan/parser.js b/dashboard/lib/streamPlan/parser.js
deleted file mode 100644
index 050e0f05619a4..0000000000000
--- a/dashboard/lib/streamPlan/parser.js
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * Copyright 2024 RisingWave Labs
- *
- * 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
- *
- * http://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.
- *
- */
-import { graphBfs } from "../algo"
-
-let cnt = 0
-function generateNewNodeId() {
- return "g" + ++cnt
-}
-
-function getNodeId(nodeProto, actorId) {
- return (
- actorId +
- ":" +
- (nodeProto.operatorId === undefined
- ? generateNewNodeId()
- : "o" + nodeProto.operatorId)
- )
-}
-
-class Node {
- constructor(id, actorId, nodeProto) {
- this.id = id
- /**
- * @type {any}
- */
- this.nodeProto = nodeProto
- /**
- * @type {Array}
- */
- this.output = output
- /**
- * @type {Node}
- */
- this.rootNode = rootNode
- /**
- * @type {number}
- */
- this.fragmentId = fragmentId
- /**
- * @type {string}
- */
- this.computeNodeAddress = computeNodeAddress
- /**
- * @type {Array}
- */
- this.representedActorList = null
- /**
- * @type {Set}
- */
- this.representedWorkNodes = null
- }
-}
-
-export default class StreamPlanParser {
- /**
- *
- * @param {[{node: any, actors: []}]} data raw response from the meta node
- */
- constructor(data, shownActorList) {
- this.actorId2Proto = new Map()
- /**
- * @type {Set}
- * @private
- */
- this.actorIdTomviewNodes = new Map()
- this.shownActorSet = new Set(shownActorList)
-
- for (let computeNodeData of data) {
- for (let singleActorProto of computeNodeData.actors) {
- if (
- shownActorList &&
- !this.shownActorSet.has(singleActorProto.actorId)
- ) {
- continue
- }
- this.actorId2Proto.set(singleActorProto.actorId, {
- computeNodeAddress: `${computeNodeData.node.host.host}:${computeNodeData.node.host.port}`,
- ...singleActorProto,
- })
- }
- }
-
- this.parsedNodeMap = new Map()
- this.parsedActorMap = new Map()
-
- for (let [_, singleActorProto] of this.actorId2Proto.entries()) {
- this.parseActor(singleActorProto)
- }
-
- this.parsedActorList = []
- for (let [_, actor] of this.parsedActorMap.entries()) {
- this.parsedActorList.push(actor)
- }
-
- /** @type {Set} */
- this.fragmentRepresentedActors = this._constructRepresentedActorList()
-
- /** @type {Map} */
- this.mvTableIdToSingleViewActorList = this._constructSingleViewMvList()
-
- /** @type {Map} */
- this.mvTableIdToChainViewActorList = this._constructChainViewMvList()
- }
-
- /**
- * Randomly select a actor to represent its
- * fragment, and append a property named `representedActorList`
- * to store all the other actors in the same fragment.
- *
- * Actors are degree of parallelism of a fragment, such that one of
- * the actor in a fragment can represent all the other actor in
- * the same fragment.
- *
- * @returns A Set containing actors representing its fragment.
- */
- _constructRepresentedActorList() {
- const fragmentId2actorList = new Map()
- let fragmentRepresentedActors = new Set()
- for (let actor of this.parsedActorList) {
- if (!fragmentId2actorList.has(actor.fragmentId)) {
- fragmentRepresentedActors.add(actor)
- fragmentId2actorList.set(actor.fragmentId, [actor])
- } else {
- fragmentId2actorList.get(actor.fragmentId).push(actor)
- }
- }
-
- for (let actor of fragmentRepresentedActors) {
- actor.representedActorList = fragmentId2actorList
- .get(actor.fragmentId)
- .sort((x) => x.actorId)
- actor.representedWorkNodes = new Set()
- for (let representedActor of actor.representedActorList) {
- representedActor.representedActorList = actor.representedActorList
- actor.representedWorkNodes.add(representedActor.computeNodeAddress)
- }
- }
- return fragmentRepresentedActors
- }
-
- _constructChainViewMvList() {
- let mvTableIdToChainViewActorList = new Map()
- let shellNodes = new Map()
- const getShellNode = (actorId) => {
- if (shellNodes.has(actorId)) {
- return shellNodes.get(actorId)
- }
- let shellNode = {
- id: actorId,
- parentNodes: [],
- nextNodes: [],
- }
- for (let node of this.parsedActorMap.get(actorId).output) {
- let nextNode = getShellNode(node.actorId)
- nextNode.parentNodes.push(shellNode)
- shellNode.nextNodes.push(nextNode)
- }
- shellNodes.set(actorId, shellNode)
- return shellNode
- }
-
- for (let actorId of this.actorId2Proto.keys()) {
- getShellNode(actorId)
- }
-
- for (let [actorId, mviewNode] of this.actorIdTomviewNodes.entries()) {
- let list = new Set()
- let shellNode = getShellNode(actorId)
- graphBfs(shellNode, (n) => {
- list.add(n.id)
- })
- graphBfs(
- shellNode,
- (n) => {
- list.add(n.id)
- },
- "parentNodes"
- )
- for (let actor of this.parsedActorMap.get(actorId).representedActorList) {
- list.add(actor.actorId)
- }
- mvTableIdToChainViewActorList.set(mviewNode.typeInfo.tableId, [
- ...list.values(),
- ])
- }
-
- return mvTableIdToChainViewActorList
- }
-
- _constructSingleViewMvList() {
- let mvTableIdToSingleViewActorList = new Map()
- let shellNodes = new Map()
- const getShellNode = (actorId) => {
- if (shellNodes.has(actorId)) {
- return shellNodes.get(actorId)
- }
- let shellNode = {
- id: actorId,
- parentNodes: [],
- }
- for (let node of this.parsedActorMap.get(actorId).output) {
- getShellNode(node.actorId).parentNodes.push(shellNode)
- }
- shellNodes.set(actorId, shellNode)
- return shellNode
- }
- for (let actor of this.parsedActorList) {
- getShellNode(actor.actorId)
- }
-
- for (let actorId of this.actorId2Proto.keys()) {
- getShellNode(actorId)
- }
-
- for (let [actorId, mviewNode] of this.actorIdTomviewNodes.entries()) {
- let list = []
- let shellNode = getShellNode(actorId)
- graphBfs(
- shellNode,
- (n) => {
- list.push(n.id)
- if (shellNode.id !== n.id && this.actorIdTomviewNodes.has(n.id)) {
- return true // stop to traverse its next nodes
- }
- },
- "parentNodes"
- )
- for (let actor of this.parsedActorMap.get(actorId).representedActorList) {
- list.push(actor.actorId)
- }
- mvTableIdToSingleViewActorList.set(mviewNode.typeInfo.tableId, list)
- }
-
- return mvTableIdToSingleViewActorList
- }
-
- newDispatcher(actorId, type, downstreamActorId) {
- return new Dispatcher(actorId, type, downstreamActorId, {
- operatorId: 100000 + actorId,
- })
- }
-
- /**
- * Parse raw data from meta node to an actor
- * @param {{
- * actorId: number,
- * fragmentId: number,
- * nodes: any,
- * dispatcher?: {type: string},
- * downstreamActorId?: any
- * }} actorProto
- * @returns {Actor}
- */
- parseActor(actorProto) {
- let actorId = actorProto.actorId
- if (this.parsedActorMap.has(actorId)) {
- return this.parsedActorMap.get(actorId)
- }
-
- let actor = new Actor(
- actorId,
- [],
- null,
- actorProto.fragmentId,
- actorProto.computeNodeAddress
- )
-
- let rootNode
- this.parsedActorMap.set(actorId, actor)
- if (actorProto.dispatcher && actorProto.dispatcher[0].type) {
- let nodeBeforeDispatcher = this.parseNode(actor.actorId, actorProto.nodes)
- rootNode = this.newDispatcher(
- actor.actorId,
- actorProto.dispatcher[0].type,
- actorProto.downstreamActorId
- )
- rootNode.nextNodes = [nodeBeforeDispatcher]
- } else {
- rootNode = this.parseNode(actorId, actorProto.nodes)
- }
- actor.rootNode = rootNode
-
- return actor
- }
-
- parseNode(actorId, nodeProto) {
- let id = getNodeId(nodeProto, actorId)
- if (this.parsedNodeMap.has(id)) {
- return this.parsedNodeMap.get(id)
- }
- let newNode = new StreamNode(id, actorId, nodeProto)
- this.parsedNodeMap.set(id, newNode)
-
- if (nodeProto.input !== undefined) {
- for (let nextNodeProto of nodeProto.input) {
- newNode.nextNodes.push(this.parseNode(actorId, nextNodeProto))
- }
- }
-
- if (newNode.type === "merge" && newNode.typeInfo.upstreamActorId) {
- for (let upStreamActorId of newNode.typeInfo.upstreamActorId) {
- if (!this.actorId2Proto.has(upStreamActorId)) {
- continue
- }
- this.parseActor(this.actorId2Proto.get(upStreamActorId)).output.push(
- newNode
- )
- }
- }
-
- if (newNode.type === "streamScan" && newNode.typeInfo.upstreamActorIds) {
- for (let upStreamActorId of newNode.typeInfo.upstreamActorIds) {
- if (!this.actorId2Proto.has(upStreamActorId)) {
- continue
- }
- this.parseActor(this.actorId2Proto.get(upStreamActorId)).output.push(
- newNode
- )
- }
- }
-
- if (newNode.type === "materialize") {
- this.actorIdTomviewNodes.set(actorId, newNode)
- }
-
- return newNode
- }
-
- getActor(actorId) {
- return this.parsedActorMap.get(actorId)
- }
-
- getOperator(operatorId) {
- return this.parsedNodeMap.get(operatorId)
- }
-
- /**
- * @returns {Array}
- */
- getParsedActorList() {
- return this.parsedActorList
- }
-}
diff --git a/dashboard/lib/streamPlan/streamChartHelper.js b/dashboard/lib/streamPlan/streamChartHelper.js
deleted file mode 100644
index 5610273cda3de..0000000000000
--- a/dashboard/lib/streamPlan/streamChartHelper.js
+++ /dev/null
@@ -1,945 +0,0 @@
-/*
- * Copyright 2024 RisingWave Labs
- *
- * 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
- *
- * http://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.
- *
- */
-import * as d3 from "d3"
-import { cloneDeep, max } from "lodash"
-import { getConnectedComponent, treeBfs } from "../algo"
-import * as color from "../color"
-import { Group } from "../graaphEngine/canvasEngine"
-import { newNumberArray } from "../util"
-import StreamPlanParser, { Actor } from "./parser"
-// Actor constant
-//
-// =======================================================
-// ^
-// | actorBoxPadding
-// v
-// --┌───────────┐
-// | │ node │
-// | │<--->radius│>───────\
-// | │ │ │
-// | └───────────┘ │
-// | │
-// | ┌───────────┐ │ ┌───────────┐
-// | │ node │ │ │ node │
-// widthUnit│<--->radius│>───────┼────────>│<--->radius│
-// | │ │ │ │ │
-// | └───────────┘ │ └───────────┘
-// | │
-// | ┌───────────┐ │
-// | │ node │ │
-// | │<--->radius│>───────/
-// | │ │
-// ---└───────────┘
-// |-----------------heightUnit---------------|
-//
-
-const SCALE_FACTOR = 0.5
-
-const operatorNodeRadius = 30 * SCALE_FACTOR // the radius of the tree nodes in an actor
-const operatorNodeStrokeWidth = 5 * SCALE_FACTOR // the stroke width of the link of the tree nodes in an actor
-const widthUnit = 230 * SCALE_FACTOR // the width of a tree node in an actor
-const heightUnit = 250 * SCALE_FACTOR // the height of a tree layer in an actor
-const actorBoxPadding = 100 * SCALE_FACTOR // box padding
-const actorBoxStroke = 15 * SCALE_FACTOR // the width of the stroke of the box
-const internalLinkStrokeWidth = 30 * SCALE_FACTOR // the width of the link between nodes
-const actorBoxRadius = 20 * SCALE_FACTOR
-
-// Stream Plan constant
-const gapBetweenRow = 100 * SCALE_FACTOR
-const gapBetweenLayer = 300 * SCALE_FACTOR
-const gapBetweenFlowChart = 500 * SCALE_FACTOR
-const outgoingLinkStrokeWidth = 20 * SCALE_FACTOR
-const outgoingLinkBgStrokeWidth = 40 * SCALE_FACTOR
-
-// Draw linking effect
-const bendGap = 50 * SCALE_FACTOR // try example at: http://bl.ocks.org/d3indepth/b6d4845973089bc1012dec1674d3aff8
-const connectionGap = 20 * SCALE_FACTOR
-
-// Others
-const fontSize = 30 * SCALE_FACTOR
-const outGoingLinkBgColor = "#eee"
-
-/**
- * Construct an id for a link in actor box.
- * You may use this method to query and get the svg element
- * of the link.
- * @param {{id: number}} node1 a node (operator) in an actor box
- * @param {{id: number}} node2 a node (operator) in an actor box
- * @returns {string} The link id
- */
-function constructInternalLinkId(node1, node2) {
- return (
- "node-" +
- (node1.id > node2.id
- ? node1.id + "-" + node2.id
- : node2.id + "-" + node1.id)
- )
-}
-
-/**
- * Construct an id for a node (operator) in an actor box.
- * You may use this method to query and get the svg element
- * of the link.
- * @param {{id: number}} node a node (operator) in an actor box
- * @returns {string} The node id
- */
-function constructOperatorNodeId(node) {
- return "node-" + node.id
-}
-
-function hashIpv4Index(addr) {
- let [ip, port] = addr.split(":")
- let s = ""
- ip.split(".").map((x) => (s += x))
- return Number(s + port)
-}
-
-export function computeNodeAddrToSideColor(addr) {
- return color.TwoGradient(hashIpv4Index(addr))[1]
-}
-
-/**
- * Work flow
- * 1. Get the layout for actor boxes (Calculate the base coordination of each actor box)
- * 2. Get the layout for operators in each actor box
- * 3. Draw all actor boxes
- * 4. Draw link between actor boxes
- *
- *
- * Dependencies
- * layoutActorBox <- dagLayout <- drawActorBox <- drawFlow
- * [ The layout of the ] [ The layout of ] [ Draw an actor ] [ Draw many actors ]
- * [ operators in an ] [ actors in a ] [ in specified ] [ and links between ]
- * [ actor. ] [ stream plan ] [ place ] [ them. ]
- *
- */
-export class StreamChartHelper {
- /**
- *
- * @param {Group} g The group element in canvas engine
- * @param {*} data The raw response from the meta node
- * @param {(e, node) => void} onNodeClick The callback function triggered when a node is click
- * @param {(e, actor) => void} onActorClick
- * @param {{type: string, node: {host: {host: string, port: number}}, id?: number}} selectedWokerNode
- * @param {Array} shownActorIdList
- */
- constructor(
- g,
- data,
- onNodeClick,
- onActorClick,
- selectedWokerNode,
- shownActorIdList
- ) {
- this.topGroup = g
- this.streamPlan = new StreamPlanParser(data, shownActorIdList)
- this.onNodeClick = onNodeClick
- this.onActorClick = onActorClick
- this.selectedWokerNode = selectedWokerNode
- this.selectedWokerNodeStr = this.selectedWokerNode
- ? selectedWokerNode.host.host + ":" + selectedWokerNode.host.port
- : "Show All"
- }
-
- getMvTableIdToSingleViewActorList() {
- return this.streamPlan.mvTableIdToSingleViewActorList
- }
-
- getMvTableIdToChainViewActorList() {
- return this.streamPlan.mvTableIdToChainViewActorList
- }
-
- /**
- * @param {Actor} actor
- * @returns
- */
- isInSelectedActor(actor) {
- if (this.selectedWokerNodeStr === "Show All") {
- // show all
- return true
- } else {
- return actor.representedWorkNodes.has(this.selectedWokerNodeStr)
- }
- }
-
- _mainColor(actor) {
- let addr = actor.representedWorkNodes.has(this.selectedWokerNodeStr)
- ? this.selectedWokerNodeStr
- : actor.computeNodeAddress
- return color.TwoGradient(hashIpv4Index(addr))[0]
- }
-
- _sideColor(actor) {
- let addr = actor.representedWorkNodes.has(this.selectedWokerNodeStr)
- ? this.selectedWokerNodeStr
- : actor.computeNodeAddress
- return color.TwoGradient(hashIpv4Index(addr))[1]
- }
-
- _operatorColor = (actor, operator) => {
- return this.isInSelectedActor(actor) && operator.type === "mviewNode"
- ? this._mainColor(actor)
- : "#eee"
- }
- _actorBoxBackgroundColor = (actor) => {
- return this.isInSelectedActor(actor) ? this._sideColor(actor) : "#eee"
- }
- _actorOutgoinglinkColor = (actor) => {
- return this.isInSelectedActor(actor) ? this._mainColor(actor) : "#fff"
- }
-
- //
- // A simple DAG layout algorithm.
- // The layout is built based on two rules.
- // 1. The link should have at two turnning points.
- // 2. The turnning point of a link should be placed
- // at the margin after the layer of its starting point.
- // -------------------------------------------------------
- // Example 1: (X)-(Z) and (Y)-(Z) is valid.
- // Row 0 (X)---------------->(Z)
- // |
- // Row 1 |
- // |
- // Row 2 (Y)---/
- // Layer 1 | Layer 2 | Layer 3
- // -------------------------------------------------------
- // Example 2: (A)-(B) is not valid.
- // Row 0 (X) /---------\ (Z)
- // | |
- // Row 1 (A)---/ (Y) |-->(B)
- //
- // Layer 1 | Layer 2 | Layer 3
- // -------------------------------------------------------
- // Example 3: (C)-(Z) is not valid
- // Row 0 (X) /-->(Z)
- // |
- // Row 1 (C)-------------/
- //
- // Layer 1 | Layer 2 | Layer 3
- // -------------------------------------------------------
- // Note that the layer of each node can be different
- // For example:
- // Row 0 ( 1) ( 3) ( 5) ( 2) ( 9)
- // Row 1 ( 4) ( 6) (10)
- // Row 2 ( 7) ( 8)
- // Layer 0 | Layer 1 | Layer 2 | Layer 3 | Layer 4 |
- //
- // Row 0 ( 1) ( 3) ( 5) ( 2) ( 9)
- // Row 1 ( 4) ( 6) (10)
- // Row 2 ( 7) ( 8)
- // Layer 0 | Layer 1 | Layer 2 | Layer 3 | Layer 4 |
- /**
- * Topological sort
- * @param {Array} nodes An array of node: {nextNodes: [...]}
- * @returns {Map} position of each node
- */
- dagLayout(nodes) {
- let sorted = []
- let _nodes = []
- let node2dagNode = new Map()
- const visit = (n) => {
- if (n.temp) {
- throw Error("This is not a DAG")
- }
- if (!n.perm) {
- n.temp = true
- let maxG = -1
- for (let nextNode of n.node.nextNodes) {
- node2dagNode.get(nextNode).isInput = false
- n.isOutput = false
- let g = visit(node2dagNode.get(nextNode))
- if (g > maxG) {
- maxG = g
- }
- }
- n.temp = false
- n.perm = true
- n.g = maxG + 1
- sorted.unshift(n.node)
- }
- return n.g
- }
- for (let node of nodes) {
- let dagNode = {
- node: node,
- temp: false,
- perm: false,
- isInput: true,
- isOutput: true,
- }
- node2dagNode.set(node, dagNode)
- _nodes.push(dagNode)
- }
- let maxLayer = 0
- for (let node of _nodes) {
- let g = visit(node)
- if (g > maxLayer) {
- maxLayer = g
- }
- }
- // use the bottom up strategy to construct generation number
- // makes the generation number of root node the samllest
- // to make the computation easier, need to flip it back.
- for (let node of _nodes) {
- // node.g = node.isInput ? 0 : (maxLayer - node.g); // TODO: determine which is more suitable
- node.g = maxLayer - node.g
- }
-
- let layers = []
- for (let i = 0; i < maxLayer + 1; ++i) {
- layers.push({
- nodes: [],
- occupyRow: new Set(),
- })
- }
- let node2Layer = new Map()
- let node2Row = new Map()
- for (let node of _nodes) {
- layers[node.g].nodes.push(node.node)
- node2Layer.set(node.node, node.g)
- }
-
- // layers to rtn
- let rtn = new Map()
-
- const putNodeInPosition = (node, row) => {
- node2Row.set(node, row)
- layers[node2Layer.get(node)].occupyRow.add(row)
- }
-
- const occupyLine = (ls, le, r) => {
- // layer start, layer end, row
- for (let i = ls; i <= le; ++i) {
- layers[i].occupyRow.add(r)
- }
- }
-
- const hasOccupied = (layer, row) => layers[layer].occupyRow.has(row)
-
- const isStraightLineOccupied = (ls, le, r) => {
- // layer start, layer end, row
- if (r < 0) {
- return false
- }
- for (let i = ls; i <= le; ++i) {
- if (hasOccupied(i, r)) {
- return true
- }
- }
- return false
- }
-
- for (let node of nodes) {
- node.nextNodes.sort((a, b) => node2Layer.get(b) - node2Layer.get(a))
- }
-
- for (let layer of layers) {
- for (let node of layer.nodes) {
- if (!node2Row.has(node)) {
- // checking node is not placed.
- for (let nextNode of node.nextNodes) {
- if (node2Row.has(nextNode)) {
- continue
- }
- let r = -1
- while (
- isStraightLineOccupied(
- node2Layer.get(node),
- node2Layer.get(nextNode),
- ++r
- )
- ) {}
- putNodeInPosition(node, r)
- putNodeInPosition(nextNode, r)
- occupyLine(
- node2Layer.get(node) + 1,
- node2Layer.get(nextNode) - 1,
- r
- )
- break
- }
- if (!node2Row.has(node)) {
- let r = -1
- while (hasOccupied(node2Layer.get(node), ++r)) {}
- putNodeInPosition(node, r)
- }
- }
- // checking node is placed in some position
- for (let nextNode of node.nextNodes) {
- if (node2Row.has(nextNode)) {
- continue
- }
- // check straight line position first
- let r = node2Row.get(node)
- if (
- !isStraightLineOccupied(
- node2Layer.get(node) + 1,
- node2Layer.get(nextNode),
- r
- )
- ) {
- putNodeInPosition(nextNode, r)
- occupyLine(
- node2Layer.get(node) + 1,
- node2Layer.get(nextNode) - 1,
- r
- )
- continue
- }
- // check lowest available position
- r = -1
- while (
- isStraightLineOccupied(
- node2Layer.get(node) + 1,
- node2Layer.get(nextNode),
- ++r
- )
- ) {}
- putNodeInPosition(nextNode, r)
- occupyLine(node2Layer.get(node) + 1, node2Layer.get(nextNode) - 1, r)
- }
- }
- }
- for (let node of nodes) {
- rtn.set(node.id, [node2Layer.get(node), node2Row.get(node)])
- }
-
- return rtn
- }
-
- /**
- * Calculate the position of each node in the actor box.
- * @param {{id: any, nextNodes: [], x: number, y: number}} rootNode The root node of an actor box (dispatcher)
- * @returns {[width, height]} The size of the actor box
- */
- calculateActorBoxSize(rootNode) {
- let rootNodeCopy = cloneDeep(rootNode)
- return this.layoutActorBox(rootNodeCopy, 0, 0)
- }
-
- /**
- * Calculate the position of each node (operator) in the actor box.
- * This will change the node's position
- * @param {{id: any, nextNodes: [], x: number, y: number}} rootNode The root node of an actor box (dispatcher)
- * @param {number} baseX The x coordination of the top-left corner of the actor box
- * @param {number} baseY The y coordination of the top-left corner of the actor box
- * @returns {[width, height]} The size of the actor box
- */
- layoutActorBox(rootNode, baseX, baseY) {
- // calculate nodes' required width
- let maxLayer = 0
- const getRequiredWidth = (node, layer) => {
- if (node.width !== undefined) {
- return node.width
- }
-
- if (layer > maxLayer) {
- maxLayer = layer
- }
-
- node.layer = layer
-
- let requiredWidth = 0
- for (let nextNode of node.nextNodes) {
- requiredWidth += getRequiredWidth(nextNode, layer + 1)
- }
-
- node.isLeaf = requiredWidth === 0
-
- node.width = requiredWidth > 0 ? requiredWidth : widthUnit
-
- return node.width
- }
-
- getRequiredWidth(rootNode, 0)
-
- // calculate nodes' position
- rootNode.x = baseX || 0
- rootNode.y = baseY || 0
- let leafY = rootNode.x - heightUnit * maxLayer
- treeBfs(rootNode, (c) => {
- let tmpY = c.y - c.width / 2
- for (let nextNode of c.nextNodes) {
- nextNode.x = nextNode.isLeaf ? leafY : c.x - heightUnit
- nextNode.y = tmpY + nextNode.width / 2
- tmpY += nextNode.width
- }
- })
-
- // calculate box size
- let minX = Infinity
- let maxX = -Infinity
- let minY = Infinity
- let maxY = -Infinity
- treeBfs(rootNode, (node) => {
- if (node.x > maxX) {
- maxX = node.x
- }
- if (node.x < minX) {
- minX = node.x
- }
- if (node.y > maxY) {
- maxY = node.y
- }
- if (node.y < minY) {
- minY = node.y
- }
- })
- let boxWidth = maxX - minX
- let boxHeight = maxY - minY
- return [boxWidth + actorBoxPadding * 2, boxHeight + actorBoxPadding * 2]
- }
-
- /**
- * @param {{
- * g: Group,
- * rootNode: {id: any, nextNodes: []},
- * nodeColor: string,
- * strokeColor?: string,
- * baseX?: number,
- * baseY?: number
- * }} props
- * @param {Group} props.g The group element in canvas engine
- * @param {{id: any, nextNodes: []}} props.rootNode The root node of the tree in the actor
- * @param {string} props.nodeColor [optional] The filled color of nodes.
- * @param {string} props.strokeColor [optional] The color of the stroke.
- * @param {number} props.baseX [optional] The x coordination of the lef-top corner. default: 0
- * @param {number} props.baseY [optional] The y coordination of the lef-top corner. default: 0
- * @returns {Group} The group element of this tree
- */
- drawActorBox(props) {
- if (props.g === undefined) {
- throw Error("Invalid Argument: Target group cannot be undefined.")
- }
-
- const actor = props.actor
- const group = props.g.append("g")
- const rootNode = props.rootNode || []
- const baseX = props.x === undefined ? 0 : props.x
- const baseY = props.y === undefined ? 0 : props.y
- const strokeColor = props.strokeColor || "white"
- const linkColor = props.linkColor || "gray"
-
- group.attr("class", actor.computeNodeAddress)
-
- const [boxWidth, boxHeight] = this.calculateActorBoxSize(rootNode)
- this.layoutActorBox(
- rootNode,
- baseX + boxWidth - actorBoxPadding,
- baseY + boxHeight / 2
- )
-
- const onNodeClicked = (e, node, actor) => {
- this.onNodeClick && this.onNodeClick(e, node, actor)
- }
-
- const onActorClick = (e, actor) => {
- this.onActorClick && this.onActorClick(e, actor)
- }
-
- /**
- * @param {Group} g actor box group
- * @param {number} x top-right corner of the label
- * @param {number} y top-right corner of the label
- * @param {Array} actorIds
- * @param {string} color
- * @returns {number} width of this label
- */
- const drawActorIdLabel = (g, x, y, actorIds, color) => {
- y = y - actorBoxStroke
- let actorStr = actorIds.toString()
- let padding = 15
- // let height = fontSize + 2 * padding;
- let gap = 30
- // let polygon = g.append("polygon");
- let textEle = g
- .append("text")(actorStr)
- .attr("font-size", fontSize)
- .position(x - padding - 5, y + padding)
- let width = textEle.getWidth() + 2 * padding
- // polygon.attr("points", `${x},${y} ${x - width - gap},${y}, ${x - width},${y + height}, ${x},${y + height}`)
- // .attr("fill", color);
- return width + gap
- }
-
- // draw box
- group.attr("id", "actor-" + actor.actorId)
- let actorRect = group.append("rect")
- for (let representedActor of actor.representedActorList) {
- actorRect.classed("actor-" + representedActor.actorId, true)
- }
- actorRect.classed("fragment-" + actor.fragmentId, true)
- actorRect
- .init(baseX, baseY, boxWidth, boxHeight)
- .attr("fill", this._actorBoxBackgroundColor(actor))
- .attr("rx", actorBoxRadius)
- .attr("stroke-width", actorBoxStroke)
- .on("click", (e) => onActorClick(e, actor))
-
- group
- .append("text")(`Fragment ${actor.fragmentId}`)
- .position(baseX, baseY - actorBoxStroke - fontSize)
- .attr("font-size", fontSize)
-
- // draw compute node label
- let computeNodeToActorIds = new Map()
- for (let representedActor of actor.representedActorList) {
- if (computeNodeToActorIds.has(representedActor.computeNodeAddress)) {
- computeNodeToActorIds
- .get(representedActor.computeNodeAddress)
- .push(representedActor.actorId)
- } else {
- computeNodeToActorIds.set(representedActor.computeNodeAddress, [
- representedActor.actorId,
- ])
- }
- }
- let labelStartX = baseX + actorBoxStroke
- for (let [addr, actorIds] of computeNodeToActorIds.entries()) {
- let w = drawActorIdLabel(
- group,
- labelStartX,
- baseY + boxHeight,
- actorIds,
- color.TwoGradient(hashIpv4Index(addr))[1]
- )
- labelStartX -= w
- }
-
- // draw links
- const linkData = []
- treeBfs(rootNode, (c) => {
- for (let nextNode of c.nextNodes) {
- linkData.push({
- sourceNode: c,
- nextNode: nextNode,
- source: [c.x, c.y],
- target: [nextNode.x, nextNode.y],
- })
- }
- })
- const linkGen = d3.linkHorizontal()
- for (let link of linkData) {
- group
- .append("path")(linkGen(link))
- .attr(
- "stroke-dasharray",
- `${internalLinkStrokeWidth / 2},${internalLinkStrokeWidth / 2}`
- )
- // .attr("d", linkGen(link))
- .attr("fill", "none")
- .attr("class", "actor-" + actor.actorId)
- .classed("internal-link", true)
- .attr("id", constructInternalLinkId(link.sourceNode, link.nextNode))
- .style("stroke-width", internalLinkStrokeWidth)
- .attr("stroke", linkColor)
- }
-
- // draw nodes
- treeBfs(rootNode, (node) => {
- node.d3Selection = group
- .append("circle")
- .init(node.x, node.y, operatorNodeRadius)
- .attr("id", constructOperatorNodeId(node))
- .attr("stroke", strokeColor)
- .attr("fill", this._operatorColor(actor, node))
- .style("cursor", "pointer")
- .style("stroke-width", operatorNodeStrokeWidth)
- .on("click", (e) => onNodeClicked(e, node, actor))
- group
- .append("text")(node.type ? node.type : node.dispatcherType)
- .position(node.x, node.y + operatorNodeRadius + 10)
- .attr("font-size", fontSize)
- })
-
- return {
- g: group,
- x: baseX - boxWidth - actorBoxPadding,
- y: baseY - boxHeight / 2 - actorBoxPadding,
- width: boxWidth + actorBoxPadding * 2,
- height: boxHeight + actorBoxPadding * 2,
- }
- }
- /**
- *
- * @param {{
- * g: Group,
- * actorDagList: Array,
- * baseX?: number,
- * baseY?: number
- * }} props
- * @param {Group} props.g The target group contains this group.
- * @param {Array} props.actorDagList A list of dag nodes constructed from actors
- * { id: actor.actorId, nextNodes: [], actor: actor }
- * @param {number} props.baseX [optional] The x coordination of left-top corner. default: 0.
- * @param {number} props.baseY [optional] The y coordination of left-top corner. default: 0.
- * @returns {{group: Group, width: number, height: number}} The size of the flow
- */
- drawFlow(props) {
- if (props.g === undefined) {
- throw Error("Invalid Argument: Target group cannot be undefined.")
- }
-
- const g = props.g
- const actorDagList = props.actorDagList || []
- const baseX = props.baseX || 0
- const baseY = props.baseY || 0
-
- let layoutPositionMapper = this.dagLayout(actorDagList)
- const actors = []
- for (let actorDag of actorDagList) {
- actors.push(actorDag.actor)
- }
-
- // calculate actor box size
- for (let actor of actors) {
- ;[actor.boxWidth, actor.boxHeight] = this.calculateActorBoxSize(
- actor.rootNode
- )
- ;[actor.layer, actor.row] = layoutPositionMapper.get(actor.actorId)
- }
-
- // calculate the minimum required width of each layer and row
- let maxRow = 0
- let maxLayer = 0
- for (let actor of actors) {
- maxLayer = max([actor.layer, maxLayer])
- maxRow = max([actor.row, maxRow])
- }
- let rowGap = newNumberArray(maxRow + 1)
- let layerGap = newNumberArray(maxLayer + 1)
- for (let actor of actors) {
- layerGap[actor.layer] = max([layerGap[actor.layer], actor.boxWidth])
- rowGap[actor.row] = max([rowGap[actor.row], actor.boxHeight])
- }
- let row2y = newNumberArray(maxRow + 1)
- let layer2x = newNumberArray(maxLayer + 1)
- row2y = row2y.map((_, r) => {
- if (r === 0) {
- return 0
- }
- let rtn = 0
- for (let i = 0; i < r; ++i) {
- rtn += rowGap[i] + gapBetweenRow
- }
- return rtn
- })
- layer2x = layer2x.map((_, l) => {
- if (l === 0) {
- return 0
- }
- let rtn = 0
- for (let i = 0; i < l; ++i) {
- rtn += layerGap[i] + gapBetweenLayer
- }
- return rtn
- })
-
- // Draw fragment (represent by one actor)
- const group = g.append("g")
- const linkLayerBackground = group.append("g")
- const linkLayer = group.append("g")
- const fragmentLayer = group.append("g")
- linkLayerBackground.attr("class", "linkLayerBackground")
- linkLayer.attr("class", "linkLayer")
- fragmentLayer.attr("class", "fragmentLayer")
-
- let actorBoxList = []
- for (let actor of actors) {
- let actorBox = this.drawActorBox({
- actor: actor,
- g: fragmentLayer,
- rootNode: actor.rootNode,
- x: baseX + layer2x[actor.layer],
- y: baseY + row2y[actor.row],
- strokeColor: "white",
- linkColor: "white",
- })
- actorBoxList.push(actorBox)
- }
-
- // Draw link between (represent by one actor)
- const getLinkBetweenPathStr = (start, end, compensation) => {
- const lineGen = d3.line().curve(d3.curveBasis)
- let pathStr = lineGen([
- end,
- [
- start[0] +
- compensation +
- actorBoxPadding +
- connectionGap +
- bendGap * 2,
- end[1],
- ],
- [
- start[0] + compensation + actorBoxPadding + connectionGap + bendGap,
- end[1],
- ],
- [
- start[0] + compensation + actorBoxPadding + connectionGap + bendGap,
- start[1],
- ],
- [start[0] + compensation + actorBoxPadding + connectionGap, start[1]],
- start,
- ])
- return pathStr
- }
-
- let linkData = []
- for (let actor of actors) {
- for (let outputNode of actor.output) {
- linkData.push({
- actor: actor,
- d: getLinkBetweenPathStr(
- [actor.rootNode.x, actor.rootNode.y],
- [outputNode.x, outputNode.y],
- layerGap[actor.layer] - actor.boxWidth
- ),
- })
- }
- }
-
- for (let s of linkData) {
- linkLayer
- .append("path")(s.d)
- .attr(
- "stroke-dasharray",
- `${outgoingLinkStrokeWidth},${outgoingLinkStrokeWidth}`
- )
- .attr("fill", "none")
- .attr("class", "actor-" + s.actor.actorId)
- .classed("outgoing-link", true)
- .style("stroke-width", outgoingLinkStrokeWidth)
- .attr("stroke", this._actorOutgoinglinkColor(s.actor))
- .attr("layer", "back")
- }
-
- for (let s of linkData) {
- linkLayerBackground
- .append("path")(s.d)
- .attr("fill", "none")
- .style("stroke-width", outgoingLinkBgStrokeWidth)
- .attr("class", "actor-" + s.actor.actorId)
- .classed("outgoing-link-bg", true)
- .attr("stroke", outGoingLinkBgColor)
- .attr("layer", "back")
- }
-
- // calculate box size
- let width = 0
- let height = 0
- for (let actorBox of actorBoxList) {
- let biggestX = actorBox.x - baseX + actorBox.width
- let biggestY = actorBox.y - baseY + actorBox.height
- width = max([biggestX, width])
- height = max([biggestY, height])
- }
-
- group.attr("class", "flowchart")
- return {
- g: group,
- width: width,
- height: height,
- }
- }
-
- /**
- * A flow is an extracted connected component of actors of
- * the raw response from the meta node. This method will first
- * merge actors in the same fragment using some identifier
- * (currently it is the id of the operator before the dispatcher).
- * And then use `drawFlow()` to draw each connected component.
- */
- drawManyFlow() {
- const g = this.topGroup
- const baseX = 0
- const baseY = 0
-
- g.attr("id", "")
-
- let fragmentRepresentedActors = this.streamPlan.fragmentRepresentedActors
- // get dag layout of these actors
- let dagNodeMap = new Map()
- for (let actor of fragmentRepresentedActors) {
- actor.rootNode.actorId = actor.actorId
- treeBfs(actor.rootNode, (node) => {
- node.actorId = actor.actorId
- })
- dagNodeMap.set(actor.actorId, {
- id: actor.actorId,
- nextNodes: [],
- actor: actor,
- })
- }
- for (let actor of fragmentRepresentedActors) {
- for (let outputActorNode of actor.output) {
- let outputDagNode = dagNodeMap.get(outputActorNode.actorId)
- if (outputDagNode) {
- // the output actor node is in a represented actor
- dagNodeMap.get(actor.actorId).nextNodes.push(outputDagNode)
- }
- }
- }
- let actorDagNodes = []
- for (let id of dagNodeMap.keys()) {
- actorDagNodes.push(dagNodeMap.get(id))
- }
-
- let actorsList = getConnectedComponent(actorDagNodes)
-
- let y = baseY
- for (let actorDagList of actorsList) {
- let flowChart = this.drawFlow({
- g: g,
- baseX: baseX,
- baseY: y,
- actorDagList: actorDagList,
- })
- y += flowChart.height + gapBetweenFlowChart
- }
- }
-}
-
-/**
- * create a graph view based on raw input from the meta node,
- * and append the svg component to the giving svg group.
- * @param {Group} g The parent group contain the graph.
- * @param {any} data Raw response from the meta node. e.g. [{node: {...}, actors: {...}}, ...]
- * @param {(clickEvent, node, actor) => void} onNodeClick callback when a node (operator) is clicked.
- * @param {{type: string, node: {host: {host: string, port: number}}, id?: number}} selectedWokerNode
- * @returns {StreamChartHelper}
- */
-export default function createView(
- engine,
- data,
- onNodeClick,
- onActorClick,
- selectedWokerNode,
- shownActorIdList
-) {
- console.log(shownActorIdList, "shownActorList")
- let streamChartHelper = new StreamChartHelper(
- engine.topGroup,
- data,
- onNodeClick,
- onActorClick,
- selectedWokerNode,
- shownActorIdList
- )
- streamChartHelper.drawManyFlow()
- return streamChartHelper
-}
diff --git a/dashboard/test/algo.test.js b/dashboard/test/algo.test.js
deleted file mode 100644
index 9fe47669fed6e..0000000000000
--- a/dashboard/test/algo.test.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright 2024 RisingWave Labs
- *
- * 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
- *
- * http://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.
- *
- */
-import { Node } from "../lib/algo"
-import { StreamChartHelper } from "../lib/streamPlan/streamChartHelper"
-
-describe("Algo", () => {
- it("should generate right dag layout", () => {
- // fake data
- let nodes = []
- for (let i = 0; i < 10; ++i) {
- nodes.push(new Node([], i + 1))
- }
- const n = (i) => nodes[i - 1]
- n(1).nextNodes = [n(2), n(3)]
- n(2).nextNodes = [n(9)]
- n(3).nextNodes = [n(5), n(10)]
- n(4).nextNodes = [n(5)]
- n(5).nextNodes = [n(6), n(7)]
- n(6).nextNodes = [n(9), n(10)]
- n(7).nextNodes = [n(8)]
-
- let dagPositionMapper = new StreamChartHelper().dagLayout(nodes)
-
- // construct map
- let maxLayer = 0
- let maxRow = 0
- for (let node of dagPositionMapper.keys()) {
- let pos = dagPositionMapper.get(node)
- maxLayer = pos[0] > maxLayer ? pos[0] : maxLayer
- maxRow = pos[1] > maxRow ? pos[1] : maxRow
- }
- let m = []
- for (let i = 0; i < maxLayer + 1; ++i) {
- m.push([])
- for (let r = 0; r < maxRow + 1; ++r) {
- m[i].push([])
- }
- }
- for (let node of dagPositionMapper.keys()) {
- let pos = dagPositionMapper.get(node)
- m[pos[0]][pos[1]] = node
- }
-
- // search
- const _search = (l, r, d) => {
- // Layer, Row
- if (l > maxLayer || r > maxRow || r < 0) {
- return false
- }
- if (m[l][r].id !== undefined) {
- return m[l][r].id === d
- }
- return _search(l + 1, r, d)
- }
-
- const canReach = (node, nextNode) => {
- let pos = dagPositionMapper.get(node)
- for (let r = 0; r <= maxRow; ++r) {
- if (_search(pos[0] + 1, r, nextNode.id)) {
- return true
- }
- }
- return false
- }
-
- //check all links
- let ok = true
- for (let node of nodes) {
- for (let nextNode of node.nextNodes) {
- if (!canReach(node, nextNode)) {
- console.error(
- `Failed to connect node ${node.id} to node ${nextNode.id}`
- )
- ok = false
- break
- }
- }
- if (!ok) {
- break
- }
- }
-
- // visualization
- // let s = "";
- // for(let r = maxRow; r >= 0; --r){
- // for(let l = 0; l <= maxLayer; ++l){
- // s += `\t${m[l][r].id ? m[l][r].id : " "}`
- // }
- // s += "\n"
- // }
- // console.log(s);
-
- expect(ok).toEqual(true)
- })
-})