From 95ec265bf3937132798d59bae0c594ec0cb0a7b6 Mon Sep 17 00:00:00 2001 From: nichita Date: Wed, 13 Aug 2025 21:41:57 +0200 Subject: [PATCH 1/9] working scale and zoom, single root node rendering --- .../Parseable/ParseableGraphController.java | 69 ++ .../Parseable/ParseableGraphRepository.java | 425 ++++++++++ ui/package-lock.json | 751 ++++++++++++++++++ ui/package.json | 2 + ui/src/compoonents/D3.tsx | 58 ++ ui/src/compoonents/Main.tsx | 23 +- ui/src/compoonents/Properties.tsx | 40 - ui/src/compoonents/Visualizer.tsx | 249 ++++++ ui/vite.config.ts | 2 +- 9 files changed, 1568 insertions(+), 51 deletions(-) create mode 100644 api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java create mode 100644 api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java create mode 100644 ui/src/compoonents/D3.tsx delete mode 100644 ui/src/compoonents/Properties.tsx create mode 100644 ui/src/compoonents/Visualizer.tsx diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java new file mode 100644 index 0000000..460c1f8 --- /dev/null +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java @@ -0,0 +1,69 @@ +package com.crowfunder.cogmaster.Parseable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("api/v1/parseable") +public class ParseableGraphController { + private final ParseableGraphRepository repository; + + public ParseableGraphController(ParseableGraphRepository repository) { + this.repository = repository; + } + + public class GraphNode { + public String path; + public ArrayList children = new ArrayList<>(); + } + + @GetMapping("all") + public ResponseEntity> resolveConfigByPath() { + var nodeMap = new HashMap(); + var rootMap = new HashMap(); + + var allRecords = repository.getAll(); + for (var configFileName : allRecords.keySet()) { + var fileRecords = allRecords.get(configFileName); + for (var path : fileRecords.keySet()) { + var entry = fileRecords.get(path); + var currentPath = path.getPath(); + var currentNode = nodeMap.computeIfAbsent(currentPath, p -> { + var n = new GraphNode(); + n.path = p; + return n; + }); + + var depthCounter = 0; + while (entry.parentReference != null) { + if (depthCounter > 100) + return ResponseEntity.internalServerError().build(); + + var parentPath = entry.parentReference.referencedEntry.path.getPath(); + var parentNode = nodeMap.computeIfAbsent(parentPath, p -> { + var n = new GraphNode(); + n.path = p; + return n; + }); + + // this happens + // if (parentNode.children.contains(currentNode)) + // return ResponseEntity.internalServerError().build(); + + parentNode.children.add(currentNode); + + entry = entry.parentReference.referencedEntry; + currentNode = parentNode; + depthCounter++; + } + final var finalCurrentNode = currentNode; + rootMap.computeIfAbsent(currentNode.path, n -> finalCurrentNode); + } + } + + return ResponseEntity.ok(rootMap.values()); + } +} diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java new file mode 100644 index 0000000..a9a5838 --- /dev/null +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java @@ -0,0 +1,425 @@ +package com.crowfunder.cogmaster.Parseable; + +import com.crowfunder.cogmaster.CogmasterConfig; +import com.crowfunder.cogmaster.Configs.ConfigEntry; +import com.crowfunder.cogmaster.Configs.ParameterArray; +import com.crowfunder.cogmaster.Configs.ParameterValue; +import com.crowfunder.cogmaster.Configs.Path; +import com.crowfunder.cogmaster.Index.Index; +import com.crowfunder.cogmaster.Parsers.ParserService; +import com.crowfunder.cogmaster.Translations.TranslationsService; +import com.crowfunder.cogmaster.Routers.RouterService; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Repository; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.*; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import static com.crowfunder.cogmaster.Utils.DOMUtil.getFirstChild; +import static com.crowfunder.cogmaster.Utils.DOMUtil.getNextNode; + +class ParseableResource { + public String name; + public Resource resource; + + public ParseableResource(String name, Resource resource) { + this.name = name; + this.resource = resource; + } +} + +class NewConfigEntry { + // Source config file name + public final String configFileName; + // comes from + public final Path path; + public String implementationType; + + public NewConfigEntryReference parentReference; + public final ArrayList childEntries; + + // // If the config is a derived config, this path points to the derived from + // // (parent) config + // public final Path parentPath; + + // does not contain parent parameters + public final ParameterArray entryParameters; + // public final ParameterArray routedParameters; + // // Non-overriden parameters pulled from all derivative (parent) configs + // public final ParameterArray derivedParameters; + + // Parameterless + public NewConfigEntry(String configFileName) { + this.configFileName = configFileName; + this.path = new Path(); + this.implementationType = ""; + this.childEntries = new ArrayList(); + // this.parentPath = new Path(); // Empty string for no derivation + this.entryParameters = new ParameterArray(); + // this.derivedParameters = new ParameterArray(); + // this.routedParameters = new ParameterArray(); + } +} + +class NewConfigEntryReference { + + private final String implementationType = "com.threerings.config.ConfigReference"; + // name of config file from where the reference was parsed + private final String sourceConfigFileName; + // comes from + private final Path path; + // Overridden parameters + private final ParameterArray parameters; + // entry pointed to by this config. populated after creation + public NewConfigEntry referencedEntry; + + public NewConfigEntryReference(String sourceConfig) { + this.path = new Path(); + this.parameters = new ParameterArray(); + this.sourceConfigFileName = sourceConfig; + } + + public Path getPath() { + return this.path; + } + + public ParameterArray getParameters() { + return this.parameters; + } + + public String getSourceConfigFileName() { + return this.sourceConfigFileName; + } + + public String getImplementationType() { + return this.implementationType; + } +} + +@Repository +class ParseableGraphRepository { + + Logger logger = LoggerFactory.getLogger(ParseableGraphRepository.class); + private ArrayList parseableResources; + private Map> parsedResources; + + public ParseableGraphRepository(CogmasterConfig cogmasterConfig) { + var parseablePath = cogmasterConfig.parsers().path(); + parsedResources = new HashMap>(); + + try { + PathMatchingResourcePatternResolver r = new PathMatchingResourcePatternResolver(); + var parserResources = r.getResources("classpath*:/" + parseablePath + "/*.xml"); + this.parseableResources = new ArrayList(); + + for (Resource resource : parserResources) { + var parserName = resource.getFilename().split("\\.")[0]; + this.parseableResources.add(new ParseableResource(parserName, resource)); + } + + } catch (IOException e) { + logger.error("Failed to load properties from specified path: /{}/*", parseablePath); + throw new RuntimeException("Failed to load properties", e); + } + } + + @PostConstruct + public void populateIndex() { + logger.info("Parsing the configs, populating ConfigIndex..."); + parseResources(); + logger.info("Finished parsing"); + + logger.info("Resolving derivations..."); + resolveEntryDependencies(); + logger.info("Finished resolving"); + } + + public void parseResources() { + for (var parseableResource : parseableResources) { + var parsedEntries = parseResource(parseableResource); + parsedResources.put(parseableResource.name, parsedEntries); + } + } + + // Parse a resource file from parseable + public Map parseResource(ParseableResource parseableResource) { + var parsedEntries = new HashMap(); + + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document doc = builder.parse(parseableResource.resource.getInputStream()); + doc.getDocumentElement().normalize(); + + // All configs start at object node + Node rootNode = doc.getElementsByTagName("object").item(0); + + if (rootNode == null) { + throw new RuntimeException("Unable to locate root \"\" node"); + } + + // Start iterating over entries + NodeList entries = rootNode.getChildNodes(); + logger.debug("Parsing \"{}\" resource file.", parseableResource.name); + logger.debug("Found {} nodes.", entries.getLength()); + for (int i = 0; i < entries.getLength(); i++) { + Node entry = entries.item(i); + + if (entry.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + var parsedEntry = parseEntry(parseableResource.name, entry); + if (parsedEntries.containsKey(parsedEntry.path)) { + logger.error("Already registered entry with path {0} from file {1}.", parsedEntry.configFileName, + parsedEntry.path); + } + parsedEntries.put(parsedEntry.path, parsedEntry); + } + } catch (ParserConfigurationException | SAXException | IOException e) { + logger.error(e.toString()); + throw new RuntimeException(e); + } + return parsedEntries; + } + + // Parses node into a NewConfigEntry object + private NewConfigEntry parseEntry(String configFileName, Node entry) { + NewConfigEntry parsedEntry = new NewConfigEntry(configFileName); + + NodeList implementationNodes = entry.getChildNodes(); + + for (int i = 0; i < implementationNodes.getLength(); i++) { + Node implementationNode = implementationNodes.item(i); + if (implementationNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + switch (implementationNode.getNodeName()) { + case "name" -> parsedEntry.path.setPath(implementationNode.getTextContent()); + case "implementation" -> { + + // Handle derived ConfigEntries + String implementationType = implementationNode.getAttributes().getNamedItem("class").getNodeValue(); + if (implementationType == null) { + logger.debug("Unable to locate implementation of \"\" node"); + implementationType = "ConfigEntry"; + } + parsedEntry.implementationType = implementationType; + if (implementationType.contains("$Derived")) { + Node parentReferenceRootNode = getFirstChild(implementationNode); + if (parentReferenceRootNode == null) { + continue; + } + if (!parentReferenceRootNode.getNodeName().equals(configFileName)) { + logger.debug("Derived config parameter root node name different from config file name."); + } + + var parsedReference = parseReference(configFileName, parentReferenceRootNode); + parsedEntry.parentReference = parsedReference; + + } else { + var parsedParameterArray = parseParameterArray(implementationNode); + parsedEntry.entryParameters.update(parsedParameterArray); + } + } + case "parameters" -> { + // Parameters are unnecessary for now. + continue; + } + default -> { + continue; + } + } + + } + return parsedEntry; + } + + private NewConfigEntryReference parseReference(String configFileName, Node referenceRoot) { + NewConfigEntryReference reference = new NewConfigEntryReference(configFileName); + NodeList implementationNodes = referenceRoot.getChildNodes(); + Node parameterRoot; + for (int i = 0; i < implementationNodes.getLength(); i++) { + Node implementationNode = implementationNodes.item(i); + if (implementationNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + switch (implementationNode.getNodeName()) { + case "name" -> reference.getPath().setPath(implementationNode.getTextContent()); + case "arguments" -> { + parameterRoot = implementationNode; + ParameterArray parameterArray = parseParameterArray(parameterRoot); + reference.getParameters().update(parameterArray); + } + default -> { + continue; + } + } + } + + return reference; + } + + // This method holds some heuristics for parsing parameters + // There are some cases when it's not a simple name and value of node read + // Notably: + // - key/value node pairs + // - repeated nodes of the same name (concealed lists) + // - values as config references + private ParameterArray parseParameterArray(Node parametersRoot) { + ParameterArray parameterArray = new ParameterArray(); + + NodeList parameterNodes = parametersRoot.getChildNodes(); + for (int i = 0; i < parameterNodes.getLength(); i++) { + Node parameterNode = parameterNodes.item(i); + if (parameterNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + String key; + ParameterValue value; + + // Heuristic 1 - Repeated nodes of the same name (concealed list) + Node nextNode = getNextNode(parameterNode); + if (nextNode != null && parameterNode.getNodeName().equals(nextNode.getNodeName())) { + List listValue = new ArrayList<>(); + listValue.add(parseParameterValue(parameterNode)); + while (nextNode != null && parameterNode.getNodeName().equals(nextNode.getNodeName())) { + i++; + if (nextNode.getNodeType() == Node.ELEMENT_NODE) { + listValue.add(parseParameterValue(nextNode)); + } + nextNode = nextNode.getNextSibling(); + while (nextNode != null && nextNode.getNodeType() != Node.ELEMENT_NODE) { + nextNode = nextNode.getNextSibling(); + i++; + } + } + key = parameterNode.getNodeName(); + value = new ParameterValue(listValue); + parameterArray.addParameter(key, value); + continue; + } + + switch (parameterNode.getNodeName()) { + + // Heuristic 2 - key/value pair + case "key" -> { + key = parameterNode.getTextContent(); + + // Seldom does it happen, but sometimes key exists without a value node + // We can't jump to default so here we go redundancy! + if (nextNode == null || !nextNode.getNodeName().equals("value")) { + key = parameterNode.getNodeName(); + value = parseParameterValue(parameterNode); + break; + } + + // Heuristic 3 - newConfigReference value + // if (nextNode.getAttributes().getNamedItem("class") != null && + // nextNode.getAttributes().getNamedItem("class").getNodeValue().contains("newConfigReference")) + // { + // value = new ParameterValue(parseReference(nextNode)); + // } else { + // } + value = parseParameterValue(nextNode); // newConfigReference values are too confusing, to be + // consulted + + } + case "value" -> { + // skip, we already took care of it. If it's orphaned - shame. + continue; + } + default -> { + key = parameterNode.getNodeName(); + value = parseParameterValue(parameterNode); + } + } + parameterArray.addParameter(key, value); + } + return parameterArray; + } + + private ParameterValue parseParameterValue(Node parameterNode) { + ParameterValue parameterValue; + + // I genuinely hate you java + // https://stackoverflow.com/questions/20089661/how-to-get-child-nodes-with-element-node-type-only/20091101 + if (((Element) parameterNode).getElementsByTagName("*").getLength() != 0) { + parameterValue = new ParameterValue(parseParameterArray(parameterNode)); + } else { + parameterValue = new ParameterValue(parameterNode.getTextContent()); + } + return parameterValue; + } + + public void resolveEntryDependencies() { + for (String configFileName : parsedResources.keySet()) { + for (Path entryPath : parsedResources.get(configFileName).keySet()) { + var configEntry = parsedResources.get(configFileName).get(entryPath); + + // Resolve derivations + resolveParent(configEntry); + + // Populate routed parameters + // configEntry.populateRoutedParameters(routerService.getRouter(configEntry)); + + // Populate name index + // String name = configEntry.getName(); + // if (name != null && !name.isEmpty()) { + // index.addNameIndexEntry(translationsService.parseTranslationString(name).orElseGet(() + // -> null), + // entryPath, + // configFileName); + // } + } + } + } + + // find and set the parent of this entry + private void resolveParent(NewConfigEntry configEntry) { + if (configEntry.parentReference == null) { + return; + } + + NewConfigEntry parentConfigEntry = parsedResources.get(configEntry.configFileName) + .get(configEntry.parentReference.getPath()); + if (parentConfigEntry == null) { + logger.error("Config entry's parent not found.Source File: {0} Entry: {1} Reference: {2} ", + configEntry.configFileName, configEntry.parentReference.getPath(), configEntry.configFileName); + } else { + configEntry.parentReference.referencedEntry = parentConfigEntry; + parentConfigEntry.childEntries.add(configEntry); + } + // ParameterArray derivedParameters = new ParameterArray(); + // while (parentConfigEntry != null) { + // derivedParameters.update(parentConfigEntry.getParameters()); // would this + // not mean the parent potentially + // // overwriting the child parameters? + // if (!parentConfigEntry.isDerived()) { + // configEntry.setDerivedImplementationType(parentConfigEntry.getImplementationType()); + // } + // parentConfigEntry = readConfigIndex(configEntry.getSourceConfig(), + // parentConfigEntry.getDerivedPath()); + // } + // configEntry.updateDerivedParameters(derivedParameters); + } + + public Map> getAll() { + return parsedResources; + } +} \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index 1d8f807..726efe6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,10 +10,12 @@ "license": "MIT", "dependencies": { "@tailwindcss/vite": "^4.1.3", + "d3": "^7.9.0", "solid-js": "^1.9.5", "tailwindcss": "^4.1.3" }, "devDependencies": { + "@types/d3": "^7.4.3", "typescript": "^5.7.2", "vite": "^6.0.0", "vite-plugin-solid": "^2.11.6" @@ -1271,12 +1273,303 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-plugin-jsx-dom-expressions": { "version": "0.39.7", "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.39.7.tgz", @@ -1375,6 +1668,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1388,6 +1690,407 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1406,6 +2109,15 @@ } } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -1545,6 +2257,27 @@ "dev": true, "license": "MIT" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-what": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", @@ -1933,6 +2666,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.40.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", @@ -1972,6 +2711,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/ui/package.json b/ui/package.json index 08515d4..db1674a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,12 +11,14 @@ }, "license": "MIT", "devDependencies": { + "@types/d3": "^7.4.3", "typescript": "^5.7.2", "vite": "^6.0.0", "vite-plugin-solid": "^2.11.6" }, "dependencies": { "@tailwindcss/vite": "^4.1.3", + "d3": "^7.9.0", "solid-js": "^1.9.5", "tailwindcss": "^4.1.3" } diff --git a/ui/src/compoonents/D3.tsx b/ui/src/compoonents/D3.tsx new file mode 100644 index 0000000..9aaddb8 --- /dev/null +++ b/ui/src/compoonents/D3.tsx @@ -0,0 +1,58 @@ +import * as d3 from "d3"; +import { createSignal, For } from "solid-js"; + +type ChartProps> = { + width: number; + height: number; + margin: number; + data: T[]; + value: (d: T) => number; +}; + +const D3 = >(p: ChartProps) => { + const pieGenerator = d3.pie().value(p.value); + const parsedData = pieGenerator(p.data); + + const radius = Math.min(p.width, p.height) / 2 - p.margin; + + const arcGenerator = d3 + .arc>() + .innerRadius(0) + .outerRadius(radius); + + const colorGenerator = d3 + .scaleSequential(d3.interpolateWarm) + .domain([0, p.data.length]); + + const arcs = parsedData.map((d, i) => ({ + path: arcGenerator(d), + data: d.data, + color: colorGenerator(i), + })); + + return ( +
+ D3 Test + + {/* */} + + {(d) => ( + + )} + + {/* */} + +
+ ); +}; + +export default D3; diff --git a/ui/src/compoonents/Main.tsx b/ui/src/compoonents/Main.tsx index 72fc308..8d7b27f 100644 --- a/ui/src/compoonents/Main.tsx +++ b/ui/src/compoonents/Main.tsx @@ -1,14 +1,17 @@ -import { type Component } from 'solid-js'; -import Properties from './Properties.jsx'; +import { type Component } from "solid-js"; +import Visualizer from "./Visualizer.jsx"; +import D3 from "./D3.jsx"; const Main: Component = () => { - - - return ( -
- -
- ); + + return ( +
+ {/* x.a}/> */} +
+ +
+
+ ); }; -export default Main; \ No newline at end of file +export default Main; diff --git a/ui/src/compoonents/Properties.tsx b/ui/src/compoonents/Properties.tsx deleted file mode 100644 index dccf93e..0000000 --- a/ui/src/compoonents/Properties.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createResource, createSignal, Show, type Component } from "solid-js"; -import { getErrorMessage } from "../util/error.jsx"; -import Search from "./Search.jsx"; - -const Properties: Component = () => { - const [inputValue, setInputValue] = createSignal(""); - const [searchKey, setSearchKey] = createSignal(true); - const [searchQuery, setSearchQuery] = createSignal(null); - - // Create a fetch function that properly uses the current searchQuery - async function fetchProperties(query: string) { - if (query === null) return null; - const url = `/api/v1/properties/key?q=${query}`; - return await fetch(url); - } - - // Create the resource with the searchQuery signal as the dependency - const [getKey] = createResource(searchQuery, fetchProperties); - - const handleSearch = () => { - // Update the searchQuery signal to trigger the fetch - setSearchQuery(inputValue()); - }; - - return ( -
-

Properties

-
- - -
-
- ); -}; - -export default Properties; diff --git a/ui/src/compoonents/Visualizer.tsx b/ui/src/compoonents/Visualizer.tsx new file mode 100644 index 0000000..27f81dc --- /dev/null +++ b/ui/src/compoonents/Visualizer.tsx @@ -0,0 +1,249 @@ +import { + createEffect, + createResource, + createSignal, + For, + type Component, +} from "solid-js"; +import Search from "./Search.jsx"; +import * as d3 from "d3"; + +interface GraphNode { + path: string; + children: GraphNode[]; +} +type GraphNodeResponse = GraphNode[]; + +const Visualizer: Component = () => { + const [inputValue, setInputValue] = createSignal(""); + const [searchQuery, setSearchQuery] = createSignal(null); + + // Pan and zoom state + const [transform, setTransform] = createSignal({ + x: 0, + y: 0, + scale: 1 + }); + const [isDragging, setIsDragging] = createSignal(false); + const [lastMousePos, setLastMousePos] = createSignal({ x: 0, y: 0 }); + + let canvasRef: HTMLCanvasElement | undefined; + + async function fetchProperties( + query: string | null + ): Promise<{ status: number; body: GraphNodeResponse } | null> { + const url = `/api/v1/parseable/all`; + const awaited = await fetch(url); + return { status: awaited.status, body: await awaited.json() }; + } + const [response, { refetch }] = createResource(searchQuery, fetchProperties); + + const handleSearch = () => { + const lastSearch = searchQuery(); + setSearchQuery(inputValue()); + if (lastSearch === searchQuery()) refetch(); + }; + + const firstElement = () => response()?.body.find(x => x.children.some(y => true)); + + const hierarchy = () => { + const root = firstElement(); + return !root ? null : d3.hierarchy(root, (x) => x.children); + }; + + const tree = () => { + const hierarchyRoot = hierarchy(); + if (!hierarchyRoot) return null; + const nodes = hierarchyRoot.count(); + console.log(nodes.value); + const treeLayout = d3.tree().size([(nodes.value ?? 10) * 15, (nodes.value ?? 10) * 15]); + return treeLayout(hierarchyRoot); + }; + + // Helper function to collect all nodes recursively + const getAllNodes = () => { + const treeData = tree(); + if (!treeData) return []; + + const nodes: d3.HierarchyPointNode[] = []; + treeData.each((node) => { + nodes.push(node); + }); + return nodes; + }; + + // Pan and zoom event handlers + const handleMouseDown = (e: MouseEvent) => { + setIsDragging(true); + setLastMousePos({ x: e.clientX, y: e.clientY }); + if (canvasRef) { + canvasRef.style.cursor = 'grabbing'; + } + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging()) return; + + const deltaX = e.clientX - lastMousePos().x; + const deltaY = e.clientY - lastMousePos().y; + + setTransform(prev => ({ + ...prev, + x: prev.x + deltaX, + y: prev.y + deltaY + })); + + setLastMousePos({ x: e.clientX, y: e.clientY }); + }; + + const handleMouseUp = () => { + setIsDragging(false); + if (canvasRef) { + canvasRef.style.cursor = 'grab'; + } + }; + + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + + const rect = canvasRef!.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1; + const newScale = Math.max(0.1, Math.min(5, transform().scale * scaleFactor)); + + // Zoom towards mouse position + const scaleRatio = newScale / transform().scale; + const newX = mouseX - (mouseX - transform().x) * scaleRatio; + const newY = mouseY - (mouseY - transform().y) * scaleRatio; + + setTransform({ + x: newX, + y: newY, + scale: newScale + }); + }; + + // Reset view function + const resetView = () => { + setTransform({ x: 200, y: 200, scale: 1 }); + }; + + createEffect(() => { + if (canvasRef && tree()) { + const ctx = canvasRef.getContext('2d'); + if (ctx) { + const currentTransform = transform(); + + // Clear canvas + ctx.clearRect(0, 0, 400, 400); + + // Save context and apply transform + ctx.save(); + ctx.translate(currentTransform.x, currentTransform.y); + ctx.scale(currentTransform.scale, currentTransform.scale); + + // Draw nodes and links + const treeData = tree()!; + + // Draw links first + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1 / currentTransform.scale; // Adjust line width for zoom + treeData.links().forEach(link => { + ctx.beginPath(); + ctx.moveTo(link.source.x!, link.source.y!); + ctx.lineTo(link.target.x!, link.target.y!); + ctx.stroke(); + }); + + // Draw nodes + ctx.fillStyle = '#69b3a2'; + treeData.each(node => { + ctx.beginPath(); + ctx.arc(node.x!, node.y!, 5 / currentTransform.scale, 0, 2 * Math.PI); // Adjust node size for zoom + ctx.fill(); + }); + + // Restore context + ctx.restore(); + } + } + }); + + // Set up event listeners when canvas is ready + createEffect(() => { + if (canvasRef) { + canvasRef.style.cursor = 'grab'; + + // Mouse events for panning + canvasRef.addEventListener('mousedown', handleMouseDown); + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + + // Wheel event for zooming + canvasRef.addEventListener('wheel', handleWheel); + + // Initialize centered view + setTransform({ x: 200, y: 200, scale: 1 }); + + // Cleanup function + return () => { + canvasRef?.removeEventListener('mousedown', handleMouseDown); + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + canvasRef?.removeEventListener('wheel', handleWheel); + }; + } + }); + + return ( +
+

Properties

+
+
+ + +
+
+ +
+ Pan: Click and drag | Zoom: Mouse wheel | Scale: {transform().scale.toFixed(2)}x +
+
+
+ Roots: {response()?.body?.length || 0} | Total: {getAllNodes().length} +
+ Root: x={tree()?.x}, y={tree()?.y} +
+ Transform: x={transform().x.toFixed(1)}, y={transform().y.toFixed(1)}, scale={transform().scale.toFixed(2)} + + {(node) => ( +
+ Path: {node.data.path} - x: {node.x}, y: {node.y} +
+ )} +
+
+
+
+ ); +}; + +export default Visualizer; \ No newline at end of file diff --git a/ui/vite.config.ts b/ui/vite.config.ts index b72306e..ba8764e 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ port: 3000, proxy: { '/api': { - target: 'http://localhost:8080', + target: 'http://localhost:2137', }, }, }, From 0ceea05adb45b8df05026b0f56ab454c058b1921 Mon Sep 17 00:00:00 2001 From: nichita Date: Tue, 26 Aug 2025 19:24:52 +0200 Subject: [PATCH 2/9] port indexController to v2 api using the graph representation(newConfigEntry) as a data source --- .../cogmaster/Assets/AssetsController.java | 1 - .../cogmaster/Assets/AssetsRepository.java | 1 - .../crowfunder/cogmaster/CogmasterConfig.java | 2 - .../crowfunder/cogmaster/Configs/Path.java | 2 - .../cogmaster/Index/IndexController2.java | 76 ++++++ .../cogmaster/Index/IndexService2.java | 237 ++++++++++++++++++ .../cogmaster/Parseable/NameIndexService.java | 88 +++++++ .../cogmaster/Parseable/NewConfigEntry.java | 66 +++++ .../Parseable/NewConfigEntryReference.java | 39 +++ .../Parseable/ParseableGraphController.java | 20 +- .../Parseable/ParseableGraphRepository.java | 193 ++++++-------- .../Parseable/ParseableResource.java | 13 + ui/src/compoonents/Visualizer.tsx | 220 ++++++++++------ 13 files changed, 754 insertions(+), 204 deletions(-) create mode 100644 api/src/main/java/com/crowfunder/cogmaster/Index/IndexController2.java create mode 100644 api/src/main/java/com/crowfunder/cogmaster/Index/IndexService2.java create mode 100644 api/src/main/java/com/crowfunder/cogmaster/Parseable/NameIndexService.java create mode 100644 api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java create mode 100644 api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntryReference.java create mode 100644 api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableResource.java diff --git a/api/src/main/java/com/crowfunder/cogmaster/Assets/AssetsController.java b/api/src/main/java/com/crowfunder/cogmaster/Assets/AssetsController.java index 4a508a8..474aaf0 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Assets/AssetsController.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Assets/AssetsController.java @@ -1,6 +1,5 @@ package com.crowfunder.cogmaster.Assets; -import com.crowfunder.cogmaster.Utils.StringResult; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; diff --git a/api/src/main/java/com/crowfunder/cogmaster/Assets/AssetsRepository.java b/api/src/main/java/com/crowfunder/cogmaster/Assets/AssetsRepository.java index 65065c7..9c2c0d7 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Assets/AssetsRepository.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Assets/AssetsRepository.java @@ -1,7 +1,6 @@ package com.crowfunder.cogmaster.Assets; import com.crowfunder.cogmaster.CogmasterConfig; -import com.crowfunder.cogmaster.Translations.TranslationsRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; diff --git a/api/src/main/java/com/crowfunder/cogmaster/CogmasterConfig.java b/api/src/main/java/com/crowfunder/cogmaster/CogmasterConfig.java index 37e1245..5eb5530 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/CogmasterConfig.java +++ b/api/src/main/java/com/crowfunder/cogmaster/CogmasterConfig.java @@ -2,8 +2,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; -import java.util.List; - @ConfigurationProperties(prefix = "cogmaster") public record CogmasterConfig(Translations translations, Routers routers, Parsers parsers, Assets assets) { diff --git a/api/src/main/java/com/crowfunder/cogmaster/Configs/Path.java b/api/src/main/java/com/crowfunder/cogmaster/Configs/Path.java index fe06aaa..af5b545 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Configs/Path.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Configs/Path.java @@ -2,9 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Objects; /** diff --git a/api/src/main/java/com/crowfunder/cogmaster/Index/IndexController2.java b/api/src/main/java/com/crowfunder/cogmaster/Index/IndexController2.java new file mode 100644 index 0000000..8b02f43 --- /dev/null +++ b/api/src/main/java/com/crowfunder/cogmaster/Index/IndexController2.java @@ -0,0 +1,76 @@ +package com.crowfunder.cogmaster.Index; + +import com.crowfunder.cogmaster.Parseable.NameIndexService; +import com.crowfunder.cogmaster.Parseable.NewConfigEntry; +import com.crowfunder.cogmaster.Parseable.ParseableGraphRepository; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +@RestController +@RequestMapping("api/v2/index") +public class IndexController2 { + + private final ParseableGraphRepository graphRepo; + private final IndexService2 indexService; + private final NameIndexService nameIndexService; + + public IndexController2(ParseableGraphRepository graphRepo, IndexService2 indexService, + NameIndexService nameIndexService) { + this.graphRepo = graphRepo; + this.indexService = indexService; + this.nameIndexService = nameIndexService; + } + + @GetMapping("config/{configName}") + public ResponseEntity resolveConfigByPath(@PathVariable("configName") String configName, + @RequestParam String path) { + return graphRepo.resolveConfig(configName, path).map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + @GetMapping("search") + public ResponseEntity> resolveConfigByName(@RequestParam String q) { + return ResponseEntity.ok(indexService.resolveConfigByName(q)); + } + + @GetMapping("info/config/names") + public ResponseEntity> getAllConfigNames() { + return ResponseEntity.ok(indexService.getAllConfigFileNames()); + } + + @GetMapping("info/config/paths") + public ResponseEntity> getAllConfigPaths() { + return ResponseEntity.ok(indexService.getAllConfigEntryKeys()); + } + + @GetMapping("info/config/map") + public ResponseEntity>> getConfigPathsMap() { + Optional>> resolvedConfigs = Optional + .ofNullable(indexService.getAllConfigIndexKeysMapped()); + return resolvedConfigs.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @GetMapping("info/search/names") + public ResponseEntity> getAllEntryNames( + @RequestParam(name = "tradeable", required = false, defaultValue = "false") boolean tradeable) { + Optional> resolvedConfigs; + if (tradeable) { + resolvedConfigs = Optional.ofNullable(indexService.getTradeableEntryNames()); + } else { + resolvedConfigs = Optional.ofNullable(nameIndexService.getNameIndexKeysPretty()); + } + return resolvedConfigs.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @GetMapping("info/stats") + public ResponseEntity> getStats() { + return ResponseEntity.ok(indexService.getIndexStats()); + } + +} diff --git a/api/src/main/java/com/crowfunder/cogmaster/Index/IndexService2.java b/api/src/main/java/com/crowfunder/cogmaster/Index/IndexService2.java new file mode 100644 index 0000000..eb7afd9 --- /dev/null +++ b/api/src/main/java/com/crowfunder/cogmaster/Index/IndexService2.java @@ -0,0 +1,237 @@ +package com.crowfunder.cogmaster.Index; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.stereotype.Service; + +import com.crowfunder.cogmaster.Configs.Path; +import com.crowfunder.cogmaster.Parseable.NameIndexService; +import com.crowfunder.cogmaster.Parseable.NewConfigEntry; +import com.crowfunder.cogmaster.Parseable.ParseableGraphRepository; + +@Service +public class IndexService2 { + + private final ParseableGraphRepository graphRepo; + private final NameIndexService nameIndexService; + + public IndexService2(ParseableGraphRepository graphRepo, NameIndexService nameIndexService) { + this.graphRepo = graphRepo; + this.nameIndexService = nameIndexService; + } + + public Set getAllConfigFileNames() { + return graphRepo.getAll().keySet(); + } + + // Returns all config index keys(from all files) concatenated into a single set + public Set getAllConfigEntryKeys() { + Set result = new HashSet<>(); + for (Map subIndex : graphRepo.getAll().values()) { + for (Path path : subIndex.keySet()) { + result.add(path.toString()); + } + } + return result; + } + + // returns a map keyed by config file name and a value being the list of all the + // config entry keys in that file + public Map> getAllConfigIndexKeysMapped() { + Map> result = new HashMap<>(); + for (Map.Entry> entry : graphRepo.getAll().entrySet()) { + String outerKey = entry.getKey(); + Set innerKeys = entry.getValue().keySet(); + Set innerKeysString = new HashSet<>(); + for (Path path : innerKeys) { + innerKeysString.add(path.toString()); + } + + result.put(outerKey, innerKeysString); + } + + return result; + } + + // Resolve one or more ConfigEntry objects by + // querying the propertiesService for name mappings + // that can be used in nameIndex + // Ignores case (always searches by lowercase) + public List resolveConfigByName(String name) { + return graphRepo.resolveConfigsFullPath(nameIndexService.readNameIndex(name)); + } + + // Copied from the old Index Service + // Endpoint tailored for Kozma Bot, with love + // Return name index keys into a single list + // Attempts to only return items that are tradeable in game + // Using a few heurestics, namely filter by implementations + // and look for known parameters defining being tradeable + public Set getTradeableEntryNames() { + class EntryNameVariants { + private static final Set variantsWeapon = new HashSet<>(Set.of( + "{0} Asi Very High", + "{0} Asi Very High Ctr Very High", + "{0} Asi Very High Ctr High", + "{0} Asi Very High Ctr Med", + "{0} Asi High", + "{0} Asi High Ctr Very High", + "{0} Asi High Ctr High", + "{0} Asi High Ctr Med", + "{0} Asi Med", + "{0} Asi Med Ctr Very High", + "{0} Asi Med Ctr High", + "{0} Asi Med Ctr Med", + "{0} Ctr Very High", + "{0} Ctr High", + "{0} Ctr Med")); + private static final Set variantsBomb = new HashSet<>(Set.of( + "{0} Ctr Very High", + "{0} Ctr High", + "{0} Ctr Med")); + private static final Set variantsArmor = new HashSet<>(Set.of( + "{0} Fire High", + "{0} Fire Max", + "{0} Shadow High", + "{0} Shadow Max", + "{0} Normal High", + "{0} Normal Max")); + private static final Set variantsShield = new HashSet<>(Set.of( + "{0} Fire High", + "{0} Fire Max")); + private static final Set implementationsBomb = new HashSet<>(Set.of( + "com.threerings.projectx.item.config.ItemConfig$Bomb")); + private static final Set implementationsWeapon = new HashSet<>(Set.of( + "com.threerings.projectx.item.config.ItemConfig$Handgun", + "com.threerings.projectx.item.config.ItemConfig$SwingingHandgun", + "com.threerings.projectx.item.config.ItemConfig$Sword")); + + private static final Set implementationsArmor = new HashSet<>(Set.of( + "com.threerings.projectx.item.config.ItemConfig$Armor", + "com.threerings.projectx.item.config.ItemConfig$Helm", + "com.threerings.projectx.item.config.ItemConfig$Shield")); + + private static final Set implementationsShield = new HashSet<>(Set.of( + "com.threerings.projectx.item.config.ItemConfig$Shield")); + + public static List getItemVariants(String name, String implementation) { + List result = new ArrayList<>(); + result.add(name); + Set variants; + if (implementationsBomb.contains(implementation)) { + variants = variantsBomb; + } else if (implementationsWeapon.contains(implementation)) { + variants = variantsWeapon; + } else if (implementationsArmor.contains(implementation)) { + variants = variantsArmor; + } else if (implementationsShield.contains(implementation)) { + variants = variantsShield; + } else { + return result; + } + for (String variant : variants) { + result.add(MessageFormat.format(variant, name)); + } + return result; + } + } + + Set implementationsWhitelist = new HashSet<>(Set.of( + "com.threerings.projectx.item.config.ItemConfig$SpawnActor", + "com.threerings.projectx.item.config.ItemConfig$AnimatedAction", + "com.threerings.projectx.item.config.ItemConfig$Armor", + "com.threerings.projectx.item.config.ItemConfig$ArmorCostume", + "com.threerings.projectx.item.config.ItemConfig$Bomb", + "com.threerings.projectx.item.config.ItemConfig$Color", + "com.threerings.projectx.item.config.ItemConfig$Craft", + "com.threerings.projectx.item.config.ItemConfig$GiftBox", + "com.threerings.projectx.item.config.ItemConfig$Handgun", + "com.threerings.projectx.item.config.ItemConfig$Height", + "com.threerings.projectx.item.config.ItemConfig$Helm", + "com.threerings.projectx.item.config.ItemConfig$HelmCostume", + "com.threerings.projectx.item.config.ItemConfig$Lockbox", + "com.threerings.projectx.item.config.ItemConfig$Shield", + "com.threerings.projectx.item.config.ItemConfig$ShieldCostume", + "com.threerings.projectx.item.config.ItemConfig$SpriteEgg", + "com.threerings.projectx.item.config.ItemConfig$SwingingHandgun", + "com.threerings.projectx.item.config.ItemConfig$Sword", + "com.threerings.projectx.item.config.ItemConfig$Trinket", + "com.threerings.projectx.item.config.ItemConfig$Ticket", + "com.threerings.projectx.item.config.ItemConfig$Upgrade", + "com.threerings.projectx.item.config.ItemConfig$WrappingPaper", + "com.threerings.projectx.design.config.FurniConfig$Prop", + "com.threerings.projectx.design.config.FurniConfig$SpecialProp", + "com.threerings.projectx.item.config.AccessoryConfig$Footstep", + "com.threerings.projectx.item.config.AccessoryConfig$Original")); + Set namesBlacklist = new HashSet<>(Set.of( + "Prototype Rocket Hammer", + "Stable Rocket Hammer", + "Warmaster Rocket Hammer", + "Dark Reprisal", + "Dark Reprisal Mk II", + "Dark Retribution", + "Groundbreaker Armor", + "Groundbreaker Helm", + "Honor Blade", + "Tempered Honor Blade", + "Ascended Honor Blade", + "Lionheart Honor Blade", + "Honor Guard", + "Great Honor Guard", + "Mighty Honor Guard", + "Exalted Honor Guard")); + + Set result = new HashSet<>(); + + for (String name : nameIndexService.getNameIndexKeysPretty()) { + // Check if name is blacklisted or null or incomplete + if (name == null || namesBlacklist.contains(name) || name.contains("{")) { + continue; + } + + // Check if implementation is whitelisted + NewConfigEntry configEntry = resolveConfigByName(name).get(0); // Get any entry, shouldn't matter for + // tradeable items + if (!implementationsWhitelist.contains(configEntry.getRootImplementationType())) { + continue; + } + + // Check if known parameters marking items as untradeable exist + if (!(configEntry.getRoutedParameters().resolveParameterPath("locked") == null)) { + continue; + } + + // Do not add rooms tickets + if (configEntry.getRoutedParameters().parameterValueEquals("type", "DESIGN_ROOM") || + configEntry.getRoutedParameters().parameterValueEquals("type", "GUILD_EXPANSION") || + configEntry.getRoutedParameters().parameterValueEquals("type", "GUILD_UPGRADE") || + configEntry.getRoutedParameters().parameterValueEquals("type", "DOOR_TYPE")) { + continue; + } + + // Do not variant 0,1-star items + if (!(configEntry.getRoutedParameters().parameterValueEquals("rarity", "1")) && + !(configEntry.getRoutedParameters().parameterValueEquals("rarity", "0"))) { + result.addAll(EntryNameVariants.getItemVariants(name, configEntry.getRootImplementationType())); + } else { + result.add(name); + } + } + return result; + } + + public Map getIndexStats() { + Map stats = new HashMap<>(); + stats.put("Parsed Configs", getAllConfigFileNames().size()); + stats.put("Loaded Config Entries", getAllConfigEntryKeys().size()); + stats.put("Named Config Entries", nameIndexService.getNameIndexKeysPretty().size()); + return stats; + } + +} diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/NameIndexService.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NameIndexService.java new file mode 100644 index 0000000..0c68ac1 --- /dev/null +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NameIndexService.java @@ -0,0 +1,88 @@ +package com.crowfunder.cogmaster.Parseable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.stereotype.Service; + +import com.crowfunder.cogmaster.Configs.ParameterValue; +import com.crowfunder.cogmaster.Configs.Path; +import com.crowfunder.cogmaster.Translations.TranslationsService; + +import jakarta.annotation.PostConstruct; + +@Service +public class NameIndexService { + + private final ParseableGraphRepository graphRepo; + private final TranslationsService translationsService; + + // Name Index mapping properties keys found in node to specific config + // paths + // names are stored in lowercase and looked up as lowercase + private final Map> nameIndex = new HashMap<>(); + + // Name index keys preserving their original case + // for use in front-end autocomplete + private final Set nameIndexKeysPretty = new HashSet<>(); + + public NameIndexService(ParseableGraphRepository graphRepository, TranslationsService translationsService) { + this.graphRepo = graphRepository; + this.translationsService = translationsService; + } + + public Map> getNameIndex() { + return nameIndex; + } + + + public List readNameIndex(String key) { + return nameIndex.getOrDefault(key.toLowerCase(), new ArrayList<>()); + } + + public Set getNameIndexKeysPretty() { + return nameIndexKeysPretty; + } + + @PostConstruct + private void resolveNamesAndPrettyNames() { + var parsedResources = graphRepo.getAll(); + for (String configFileName : parsedResources.keySet()) { + for (Path entryPath : parsedResources.get(configFileName).keySet()) { + var configEntry = parsedResources.get(configFileName).get(entryPath); + + // Populate name index + ParameterValue name = configEntry.routedParameters.resolveParameterPath("name"); + var nameString = name == null ? null : name.toString(); + + if (nameString != null && !nameString.isEmpty()) { + var prependedPath = entryPath.prependedPath(configFileName); + var translatedName = translationsService.parseTranslationString(nameString).orElseGet(() -> null); + addNameIndexEntry(translatedName, prependedPath); + } + } + } + } + + private void addNameIndexEntry(String name, Path path) { + if (name == null) { + return; + } + nameIndexKeysPretty.add(name); + name = name.toLowerCase(); + if (nameIndex.get(name) == null) { + initializeNameIndex(name); + } + + nameIndex.get(name).add(path); + } + + private void initializeNameIndex(String name) { + this.nameIndex.put(name, new ArrayList<>()); + } + +} diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java new file mode 100644 index 0000000..44275be --- /dev/null +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java @@ -0,0 +1,66 @@ +package com.crowfunder.cogmaster.Parseable; + +import java.util.ArrayList; + +import com.crowfunder.cogmaster.Configs.ParameterArray; +import com.crowfunder.cogmaster.Configs.ParameterValue; +import com.crowfunder.cogmaster.Configs.Path; + +public class NewConfigEntry { + // name of config file from where the entry was parsed + public final String configFileName; + // comes from + public final Path path; + public String implementationType; + + public NewConfigEntryReference parentReference; + public final ArrayList childEntries; + + // does not contain parent parameters + public final ParameterArray entryParameters; + public final ParameterArray routedParameters; + // // Non-overriden parameters pulled from all derivative (parent) configs + // public final ParameterArray derivedParameters; + + // Parameterless + public NewConfigEntry(String configFileName) { + this.configFileName = configFileName; + this.path = new Path(); + this.implementationType = ""; + this.childEntries = new ArrayList(); + this.entryParameters = new ParameterArray(); + this.routedParameters = new ParameterArray(); + // this.derivedParameters = new ParameterArray(); + } + + // get the implementation type of the very first parent(root) node + public String getRootImplementationType() { + if (parentReference == null) { + return implementationType; + } + return parentReference.referencedEntry.getRootImplementationType(); + } + + // returns parameters, overriding parent parameter with child's if they exist + public ParameterArray getEffectiveParameters() { + if (parentReference == null) + return entryParameters; + + // accumulate params, entryParams take priority over the parentReference's + // params + return entryParameters.derive(parentReference.referencedEntry.getEffectiveParameters()); + } + + // Return effective name using routes + public String getName() { + ParameterValue name = routedParameters.resolveParameterPath("name"); + if (name != null) { + return name.toString(); + } + return null; + } + + public ParameterArray getRoutedParameters() { + return this.routedParameters; + } +} diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntryReference.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntryReference.java new file mode 100644 index 0000000..876cc68 --- /dev/null +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntryReference.java @@ -0,0 +1,39 @@ +package com.crowfunder.cogmaster.Parseable; + +import com.crowfunder.cogmaster.Configs.ParameterArray; +import com.crowfunder.cogmaster.Configs.Path; + +public class NewConfigEntryReference { + + private final String implementationType = "com.threerings.config.ConfigReference"; + // name of config file from where the reference was parsed + private final String sourceConfigFileName; + // comes from + private final Path path; + // Overridden parameters + private final ParameterArray parameters; + // entry pointed to by this config. populated after creation + public NewConfigEntry referencedEntry; + + public NewConfigEntryReference(String sourceConfig) { + this.path = new Path(); + this.parameters = new ParameterArray(); + this.sourceConfigFileName = sourceConfig; + } + + public Path getPath() { + return this.path; + } + + public ParameterArray getParameters() { + return this.parameters; + } + + public String getSourceConfigFileName() { + return this.sourceConfigFileName; + } + + public String getImplementationType() { + return this.implementationType; + } +} diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java index 460c1f8..0d78244 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("api/v1/parseable") +@RequestMapping("api/v1/parseableGraph") public class ParseableGraphController { private final ParseableGraphRepository repository; @@ -21,9 +21,9 @@ public class GraphNode { } @GetMapping("all") - public ResponseEntity> resolveConfigByPath() { - var nodeMap = new HashMap(); - var rootMap = new HashMap(); + public ResponseEntity> getAllconfigs() { + var allNodesMap = new HashMap(); + var nodeRootsMap = new HashMap(); var allRecords = repository.getAll(); for (var configFileName : allRecords.keySet()) { @@ -31,7 +31,7 @@ public ResponseEntity> resolveConfigByPath() { for (var path : fileRecords.keySet()) { var entry = fileRecords.get(path); var currentPath = path.getPath(); - var currentNode = nodeMap.computeIfAbsent(currentPath, p -> { + var currentNode = allNodesMap.computeIfAbsent(currentPath, p -> { var n = new GraphNode(); n.path = p; return n; @@ -40,16 +40,16 @@ public ResponseEntity> resolveConfigByPath() { var depthCounter = 0; while (entry.parentReference != null) { if (depthCounter > 100) - return ResponseEntity.internalServerError().build(); + return ResponseEntity.internalServerError().build(); // never happens var parentPath = entry.parentReference.referencedEntry.path.getPath(); - var parentNode = nodeMap.computeIfAbsent(parentPath, p -> { + var parentNode = allNodesMap.computeIfAbsent(parentPath, p -> { var n = new GraphNode(); n.path = p; return n; }); - // this happens + // this does happen // if (parentNode.children.contains(currentNode)) // return ResponseEntity.internalServerError().build(); @@ -60,10 +60,10 @@ public ResponseEntity> resolveConfigByPath() { depthCounter++; } final var finalCurrentNode = currentNode; - rootMap.computeIfAbsent(currentNode.path, n -> finalCurrentNode); + nodeRootsMap.computeIfAbsent(currentNode.path, n -> finalCurrentNode); } } - return ResponseEntity.ok(rootMap.values()); + return ResponseEntity.ok(nodeRootsMap.values()); } } diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java index a9a5838..d47ea1c 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java @@ -1,13 +1,10 @@ package com.crowfunder.cogmaster.Parseable; import com.crowfunder.cogmaster.CogmasterConfig; -import com.crowfunder.cogmaster.Configs.ConfigEntry; +import com.crowfunder.cogmaster.Configs.ConfigReference; import com.crowfunder.cogmaster.Configs.ParameterArray; import com.crowfunder.cogmaster.Configs.ParameterValue; import com.crowfunder.cogmaster.Configs.Path; -import com.crowfunder.cogmaster.Index.Index; -import com.crowfunder.cogmaster.Parsers.ParserService; -import com.crowfunder.cogmaster.Translations.TranslationsService; import com.crowfunder.cogmaster.Routers.RouterService; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; @@ -31,92 +28,16 @@ import static com.crowfunder.cogmaster.Utils.DOMUtil.getFirstChild; import static com.crowfunder.cogmaster.Utils.DOMUtil.getNextNode; -class ParseableResource { - public String name; - public Resource resource; - - public ParseableResource(String name, Resource resource) { - this.name = name; - this.resource = resource; - } -} - -class NewConfigEntry { - // Source config file name - public final String configFileName; - // comes from - public final Path path; - public String implementationType; - - public NewConfigEntryReference parentReference; - public final ArrayList childEntries; - - // // If the config is a derived config, this path points to the derived from - // // (parent) config - // public final Path parentPath; - - // does not contain parent parameters - public final ParameterArray entryParameters; - // public final ParameterArray routedParameters; - // // Non-overriden parameters pulled from all derivative (parent) configs - // public final ParameterArray derivedParameters; - - // Parameterless - public NewConfigEntry(String configFileName) { - this.configFileName = configFileName; - this.path = new Path(); - this.implementationType = ""; - this.childEntries = new ArrayList(); - // this.parentPath = new Path(); // Empty string for no derivation - this.entryParameters = new ParameterArray(); - // this.derivedParameters = new ParameterArray(); - // this.routedParameters = new ParameterArray(); - } -} - -class NewConfigEntryReference { - - private final String implementationType = "com.threerings.config.ConfigReference"; - // name of config file from where the reference was parsed - private final String sourceConfigFileName; - // comes from - private final Path path; - // Overridden parameters - private final ParameterArray parameters; - // entry pointed to by this config. populated after creation - public NewConfigEntry referencedEntry; - - public NewConfigEntryReference(String sourceConfig) { - this.path = new Path(); - this.parameters = new ParameterArray(); - this.sourceConfigFileName = sourceConfig; - } - - public Path getPath() { - return this.path; - } - - public ParameterArray getParameters() { - return this.parameters; - } - - public String getSourceConfigFileName() { - return this.sourceConfigFileName; - } - - public String getImplementationType() { - return this.implementationType; - } -} - @Repository -class ParseableGraphRepository { +public class ParseableGraphRepository { Logger logger = LoggerFactory.getLogger(ParseableGraphRepository.class); + private final RouterService routerService; private ArrayList parseableResources; private Map> parsedResources; - public ParseableGraphRepository(CogmasterConfig cogmasterConfig) { + public ParseableGraphRepository(CogmasterConfig cogmasterConfig, RouterService routerService) { + this.routerService = routerService; var parseablePath = cogmasterConfig.parsers().path(); parsedResources = new HashMap>(); @@ -137,7 +58,7 @@ public ParseableGraphRepository(CogmasterConfig cogmasterConfig) { } @PostConstruct - public void populateIndex() { + public void populateRepo() { logger.info("Parsing the configs, populating ConfigIndex..."); parseResources(); logger.info("Finished parsing"); @@ -145,12 +66,21 @@ public void populateIndex() { logger.info("Resolving derivations..."); resolveEntryDependencies(); logger.info("Finished resolving"); + + logger.info("Populating routed params..."); + populateRoutedParameters(); + logger.info("Finished resolving"); + } public void parseResources() { + var counter = 0; for (var parseableResource : parseableResources) { + logger.debug("File: " + parseableResource.name); var parsedEntries = parseResource(parseableResource); parsedResources.put(parseableResource.name, parsedEntries); + counter++; + logger.debug("Parsed " + counter + " entires."); } } @@ -163,7 +93,7 @@ public Map parseResource(ParseableResource parseableResour Document doc = builder.parse(parseableResource.resource.getInputStream()); doc.getDocumentElement().normalize(); - // All configs start at object node + // All config entires are contained within the object node Node rootNode = doc.getElementsByTagName("object").item(0); if (rootNode == null) { @@ -248,6 +178,7 @@ private NewConfigEntry parseEntry(String configFileName, Node entry) { return parsedEntry; } + // parses the parent reference info from an node private NewConfigEntryReference parseReference(String configFileName, Node referenceRoot) { NewConfigEntryReference reference = new NewConfigEntryReference(configFileName); NodeList implementationNodes = referenceRoot.getChildNodes(); @@ -368,24 +299,35 @@ private ParameterValue parseParameterValue(Node parameterNode) { } public void resolveEntryDependencies() { + var counter = 0; for (String configFileName : parsedResources.keySet()) { + logger.debug("File: " + configFileName); for (Path entryPath : parsedResources.get(configFileName).keySet()) { var configEntry = parsedResources.get(configFileName).get(entryPath); - - // Resolve derivations resolveParent(configEntry); + logger.debug("Resolved parent for entry " + counter + ":" + configEntry.path); + // name index and pretty name index now cached by {@link NameIndexService} + } + } + } - // Populate routed parameters - // configEntry.populateRoutedParameters(routerService.getRouter(configEntry)); - - // Populate name index - // String name = configEntry.getName(); - // if (name != null && !name.isEmpty()) { - // index.addNameIndexEntry(translationsService.parseTranslationString(name).orElseGet(() - // -> null), - // entryPath, - // configFileName); - // } + public void populateRoutedParameters() { + var counter = 0; + for (String configFileName : parsedResources.keySet()) { + logger.debug("File: " + configFileName); + for (Path entryPath : parsedResources.get(configFileName).keySet()) { + var configEntry = parsedResources.get(configFileName).get(entryPath); + + var sourceRouter = routerService.getRouter(configEntry.getRootImplementationType()); + + if (sourceRouter != null) { + for (Map.Entry e : sourceRouter.getRoutes().entrySet()) { + var effectiveParameterFlex = configEntry.getEffectiveParameters() + .resolveParameterPathFlex(e.getValue()); + configEntry.routedParameters.addParameter(e.getKey(), effectiveParameterFlex); + } + } + logger.debug("Resolved rooted params for entry " + counter + ":" + configEntry.path); } } } @@ -405,21 +347,50 @@ private void resolveParent(NewConfigEntry configEntry) { configEntry.parentReference.referencedEntry = parentConfigEntry; parentConfigEntry.childEntries.add(configEntry); } - // ParameterArray derivedParameters = new ParameterArray(); - // while (parentConfigEntry != null) { - // derivedParameters.update(parentConfigEntry.getParameters()); // would this - // not mean the parent potentially - // // overwriting the child parameters? - // if (!parentConfigEntry.isDerived()) { - // configEntry.setDerivedImplementationType(parentConfigEntry.getImplementationType()); - // } - // parentConfigEntry = readConfigIndex(configEntry.getSourceConfig(), - // parentConfigEntry.getDerivedPath()); - // } - // configEntry.updateDerivedParameters(derivedParameters); } public Map> getAll() { return parsedResources; } + + // Get ConfigEntry object by its config path + public Optional resolveConfig(String configFileName, String entryPath) { + return resolveConfig(configFileName, new Path(entryPath)); + } + + // Get ConfigEntry object by its config path + public Optional resolveConfig(String configFileName, Path entryPath) { + return readConfigIndex(configFileName, entryPath); + } + + public Optional readConfigIndex(String configFileName, Path entryPath) { + return Optional.ofNullable(parsedResources.get(configFileName)) + .map(entryMap -> entryMap.get(entryPath)); + } + + // Get ConfigEntry by path that leads both to the correct index and entry + public Optional resolveConfigFullPath(Path fileAndEntryPath) { + return readConfigIndex(fileAndEntryPath.getNextPath(), fileAndEntryPath.rotatePath()); + } + + // Get ConfigEntry by path that leads both to the correct index and entry within + public Optional resolveConfigFullPath(String fileAndEntryPath) { + return resolveConfigFullPath(new Path(fileAndEntryPath)); + } + + // Get ConfigEntry object by resolving a ConfigReference object + public Optional resolveConfig(ConfigReference configReference) { + return readConfigIndex(configReference.getSourceConfig(), configReference.getPath()); + } + + // Get multiple ConfigEntry objects by paths + // Works only for full paths (indicating the exact PathIndex entry) + public List resolveConfigsFullPath(List paths) { + List configs = new ArrayList<>(); + for (Path path : paths) { + resolveConfigFullPath(path).ifPresent(entry -> configs.add(entry)); + } + return configs; + } + } \ No newline at end of file diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableResource.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableResource.java new file mode 100644 index 0000000..9b35b0b --- /dev/null +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableResource.java @@ -0,0 +1,13 @@ +package com.crowfunder.cogmaster.Parseable; + +import org.springframework.core.io.Resource; + +public class ParseableResource { + public String name; + public Resource resource; + + public ParseableResource(String name, Resource resource) { + this.name = name; + this.resource = resource; + } +} diff --git a/ui/src/compoonents/Visualizer.tsx b/ui/src/compoonents/Visualizer.tsx index 27f81dc..23e3701 100644 --- a/ui/src/compoonents/Visualizer.tsx +++ b/ui/src/compoonents/Visualizer.tsx @@ -3,9 +3,9 @@ import { createResource, createSignal, For, + Show, type Component, } from "solid-js"; -import Search from "./Search.jsx"; import * as d3 from "d3"; interface GraphNode { @@ -14,25 +14,19 @@ interface GraphNode { } type GraphNodeResponse = GraphNode[]; +interface HoverableGraphNode extends GraphNode { + isHovered: boolean; + children: HoverableGraphNode[]; +} + const Visualizer: Component = () => { const [inputValue, setInputValue] = createSignal(""); const [searchQuery, setSearchQuery] = createSignal(null); - - // Pan and zoom state - const [transform, setTransform] = createSignal({ - x: 0, - y: 0, - scale: 1 - }); - const [isDragging, setIsDragging] = createSignal(false); - const [lastMousePos, setLastMousePos] = createSignal({ x: 0, y: 0 }); - - let canvasRef: HTMLCanvasElement | undefined; async function fetchProperties( query: string | null - ): Promise<{ status: number; body: GraphNodeResponse } | null> { - const url = `/api/v1/parseable/all`; + ): Promise<{ status: number; body: HoverableGraphNode[] } | null> { + const url = `/api/v1/parseableGraph/all`; const awaited = await fetch(url); return { status: awaited.status, body: await awaited.json() }; } @@ -44,7 +38,8 @@ const Visualizer: Component = () => { if (lastSearch === searchQuery()) refetch(); }; - const firstElement = () => response()?.body.find(x => x.children.some(y => true)); + const firstElement = () => + response()?.body.find((x) => x.children.some((y) => true)); const hierarchy = () => { const root = firstElement(); @@ -55,73 +50,109 @@ const Visualizer: Component = () => { const hierarchyRoot = hierarchy(); if (!hierarchyRoot) return null; const nodes = hierarchyRoot.count(); - console.log(nodes.value); - const treeLayout = d3.tree().size([(nodes.value ?? 10) * 15, (nodes.value ?? 10) * 15]); + const treeLayout = d3 + .tree() + .size([(nodes.value ?? 10) * 15, (nodes.value ?? 10) * 15]); return treeLayout(hierarchyRoot); }; - // Helper function to collect all nodes recursively + // Helper function to collect all nodes recursively const getAllNodes = () => { const treeData = tree(); if (!treeData) return []; - - const nodes: d3.HierarchyPointNode[] = []; + + const nodes: d3.HierarchyPointNode[] = []; treeData.each((node) => { nodes.push(node); }); return nodes; }; + const [transform, setTransform] = createSignal({ + x: 0, + y: 0, + scale: 1, + }); + const [isDragging, setIsDragging] = createSignal(false); + const [lastMousePos, setLastMousePos] = createSignal({ x: 0, y: 0 }); + + let canvasRef: HTMLCanvasElement | undefined; + + const [currentMousePos, setCurrentMousePos] = createSignal<{ + x: number; + y: number; + } | null>(null); + const [hoveredNode, setHoveredNode] = createSignal< + HoverableGraphNode | undefined + >(undefined); + // Pan and zoom event handlers const handleMouseDown = (e: MouseEvent) => { setIsDragging(true); setLastMousePos({ x: e.clientX, y: e.clientY }); if (canvasRef) { - canvasRef.style.cursor = 'grabbing'; + canvasRef.style.cursor = "grabbing"; } }; const handleMouseMove = (e: MouseEvent) => { + const rect = canvasRef!.getBoundingClientRect(); + setCurrentMousePos({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }); + if (!isDragging()) return; - + const deltaX = e.clientX - lastMousePos().x; const deltaY = e.clientY - lastMousePos().y; - - setTransform(prev => ({ + + setTransform((prev) => ({ ...prev, x: prev.x + deltaX, - y: prev.y + deltaY + y: prev.y + deltaY, })); - + setLastMousePos({ x: e.clientX, y: e.clientY }); }; + const handleMouseLeave = () => { + setHoveredNode(undefined); + if (canvasRef) { + canvasRef.style.cursor = "default"; + canvasRef.title = ""; + } + }; + const handleMouseUp = () => { setIsDragging(false); if (canvasRef) { - canvasRef.style.cursor = 'grab'; + canvasRef.style.cursor = "grab"; } }; const handleWheel = (e: WheelEvent) => { e.preventDefault(); - + const rect = canvasRef!.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; - + const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1; - const newScale = Math.max(0.1, Math.min(5, transform().scale * scaleFactor)); - + const newScale = Math.max( + 0.1, + Math.min(5, transform().scale * scaleFactor) + ); + // Zoom towards mouse position const scaleRatio = newScale / transform().scale; const newX = mouseX - (mouseX - transform().x) * scaleRatio; const newY = mouseY - (mouseY - transform().y) * scaleRatio; - + setTransform({ x: newX, y: newY, - scale: newScale + scale: newScale, }); }; @@ -130,41 +161,67 @@ const Visualizer: Component = () => { setTransform({ x: 200, y: 200, scale: 1 }); }; - createEffect(() => { + // draw frame + createEffect(() => { + console.log("draw frame"); if (canvasRef && tree()) { - const ctx = canvasRef.getContext('2d'); - if (ctx) { + const ctx = canvasRef.getContext("2d"); + if (ctx) { const currentTransform = transform(); - + // Clear canvas ctx.clearRect(0, 0, 400, 400); - + // Save context and apply transform ctx.save(); ctx.translate(currentTransform.x, currentTransform.y); ctx.scale(currentTransform.scale, currentTransform.scale); - + // Draw nodes and links const treeData = tree()!; - + // Draw links first - ctx.strokeStyle = '#999'; + ctx.strokeStyle = "#999"; ctx.lineWidth = 1 / currentTransform.scale; // Adjust line width for zoom - treeData.links().forEach(link => { + treeData.links().forEach((link) => { ctx.beginPath(); ctx.moveTo(link.source.x!, link.source.y!); ctx.lineTo(link.target.x!, link.target.y!); ctx.stroke(); }); - - // Draw nodes - ctx.fillStyle = '#69b3a2'; - treeData.each(node => { + + const mousePos = currentMousePos(); + let newHoveredNode: HoverableGraphNode | undefined = undefined; + // Calculate canvas coordinates once if we have mouse position + let canvasX: number, canvasY: number, radiusSquared: number; + if (mousePos) { + canvasX = (mousePos.x - currentTransform.x) / currentTransform.scale; + canvasY = (mousePos.y - currentTransform.y) / currentTransform.scale; + radiusSquared = Math.pow(8 / currentTransform.scale, 2); + } + // Draw nodes and hover detect + ctx.fillStyle = "#69b3a2"; + treeData.each((node) => { + ctx.fillStyle = "#69b3a2"; ctx.beginPath(); - ctx.arc(node.x!, node.y!, 5 / currentTransform.scale, 0, 2 * Math.PI); // Adjust node size for zoom + const nodeRadius = 5 / currentTransform.scale; // Adjust node size for zoom + ctx.arc(node.x!, node.y!, nodeRadius, 0, 2 * Math.PI); + + node.data.isHovered = false; + // Hit test during drawing + if (mousePos) { + const dx = canvasX - node.x!; + const dy = canvasY - node.y!; + if (dx * dx + dy * dy <= radiusSquared) { + node.data.isHovered = true; + newHoveredNode = node.data; + ctx.fillStyle = "#ffffff"; + } + } + ctx.fill(); }); - + // Restore context ctx.restore(); } @@ -174,41 +231,43 @@ const Visualizer: Component = () => { // Set up event listeners when canvas is ready createEffect(() => { if (canvasRef) { - canvasRef.style.cursor = 'grab'; - + canvasRef.style.cursor = "grab"; + // Mouse events for panning - canvasRef.addEventListener('mousedown', handleMouseDown); - window.addEventListener('mousemove', handleMouseMove); - window.addEventListener('mouseup', handleMouseUp); - + canvasRef.addEventListener("mousedown", handleMouseDown); + canvasRef.addEventListener("mousemove", handleMouseMove); + canvasRef.addEventListener("mouseup", handleMouseUp); + canvasRef.addEventListener("mouseleave", handleMouseLeave); + // Wheel event for zooming - canvasRef.addEventListener('wheel', handleWheel); - + canvasRef.addEventListener("wheel", handleWheel); + // Initialize centered view setTransform({ x: 200, y: 200, scale: 1 }); - + // Cleanup function return () => { - canvasRef?.removeEventListener('mousedown', handleMouseDown); - window.removeEventListener('mousemove', handleMouseMove); - window.removeEventListener('mouseup', handleMouseUp); - canvasRef?.removeEventListener('wheel', handleWheel); + canvasRef?.removeEventListener("mousedown", handleMouseDown); + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + canvasRef?.removeEventListener("wheel", handleWheel); }; } }); + const showDebug = false; return (

Properties

- +
-
- Pan: Click and drag | Zoom: Mouse wheel | Scale: {transform().scale.toFixed(2)}x + Pan: Click and drag | Zoom: Mouse wheel | Scale:{" "} + {transform().scale.toFixed(2)}x
+
Roots: {response()?.body?.length || 0} | Total: {getAllNodes().length}
Root: x={tree()?.x}, y={tree()?.y}
- Transform: x={transform().x.toFixed(1)}, y={transform().y.toFixed(1)}, scale={transform().scale.toFixed(2)} + Transform: x={transform().x.toFixed(1)}, y={transform().y.toFixed(1)}, + scale={transform().scale.toFixed(2)}, +
+ Mouse: x= {currentMousePos()?.x.toFixed(2)} y= + {currentMousePos()?.y.toFixed(2)} {(node) => ( -
+
Path: {node.data.path} - x: {node.x}, y: {node.y}
- )} + )}
+
); }; -export default Visualizer; \ No newline at end of file +export default Visualizer; From e83726b3bc55fca9aca512ce3edb07c0c6d39dd0 Mon Sep 17 00:00:00 2001 From: nichita Date: Sat, 6 Sep 2025 23:22:42 +0200 Subject: [PATCH 3/9] ui to test index endpoints locally --- .../cogmaster/Parseable/NewConfigEntry.java | 4 +- .../Parseable/ParseableGraphRepository.java | 2 +- ui/src/compoonents/ConfigFileNamesDisplay.tsx | 102 +++++++ ui/src/compoonents/ConfigPaths.tsx | 93 ++++++ ui/src/compoonents/ConfigPathsByFile.tsx | 103 +++++++ ui/src/compoonents/ConfigSearchForm.tsx | 81 +++++ ui/src/compoonents/D3.tsx | 58 ---- ui/src/compoonents/Main.tsx | 71 ++++- ui/src/compoonents/Search.tsx | 81 ----- ui/src/compoonents/SearchNamesForm.tsx | 121 ++++++++ ui/src/compoonents/SearchResult.tsx | 75 +++++ ui/src/compoonents/SearchResultDisplayV1.tsx | 58 ++++ ui/src/compoonents/SearchResultDisplayV2.tsx | 82 +++++ ui/src/compoonents/StatsForm.tsx | 91 ++++++ ui/src/compoonents/Toggleable.tsx | 35 +++ ui/src/compoonents/TreeViewer.tsx | 289 ++++++++++++++++++ ui/src/compoonents/Visualizer.tsx | 282 +---------------- ui/src/util/configEntry.tsx | 18 ++ ui/src/util/constants.tsx | 4 + ui/src/util/newConfigEntry.tsx | 19 ++ ui/src/util/parameterArray.tsx | 12 + ui/src/util/searchState.tsx | 11 + 22 files changed, 1267 insertions(+), 425 deletions(-) create mode 100644 ui/src/compoonents/ConfigFileNamesDisplay.tsx create mode 100644 ui/src/compoonents/ConfigPaths.tsx create mode 100644 ui/src/compoonents/ConfigPathsByFile.tsx create mode 100644 ui/src/compoonents/ConfigSearchForm.tsx delete mode 100644 ui/src/compoonents/D3.tsx delete mode 100644 ui/src/compoonents/Search.tsx create mode 100644 ui/src/compoonents/SearchNamesForm.tsx create mode 100644 ui/src/compoonents/SearchResult.tsx create mode 100644 ui/src/compoonents/SearchResultDisplayV1.tsx create mode 100644 ui/src/compoonents/SearchResultDisplayV2.tsx create mode 100644 ui/src/compoonents/StatsForm.tsx create mode 100644 ui/src/compoonents/Toggleable.tsx create mode 100644 ui/src/compoonents/TreeViewer.tsx create mode 100644 ui/src/util/configEntry.tsx create mode 100644 ui/src/util/constants.tsx create mode 100644 ui/src/util/newConfigEntry.tsx create mode 100644 ui/src/util/parameterArray.tsx create mode 100644 ui/src/util/searchState.tsx diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java index 44275be..85a3599 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java @@ -14,7 +14,7 @@ public class NewConfigEntry { public String implementationType; public NewConfigEntryReference parentReference; - public final ArrayList childEntries; + // public final ArrayList childEntries; // temporarily disable populating parent to child paths // does not contain parent parameters public final ParameterArray entryParameters; @@ -27,7 +27,7 @@ public NewConfigEntry(String configFileName) { this.configFileName = configFileName; this.path = new Path(); this.implementationType = ""; - this.childEntries = new ArrayList(); + // this.childEntries = new ArrayList(); this.entryParameters = new ParameterArray(); this.routedParameters = new ParameterArray(); // this.derivedParameters = new ParameterArray(); diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java index d47ea1c..53385d4 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphRepository.java @@ -345,7 +345,7 @@ private void resolveParent(NewConfigEntry configEntry) { configEntry.configFileName, configEntry.parentReference.getPath(), configEntry.configFileName); } else { configEntry.parentReference.referencedEntry = parentConfigEntry; - parentConfigEntry.childEntries.add(configEntry); + // parentConfigEntry.childEntries.add(configEntry); } } diff --git a/ui/src/compoonents/ConfigFileNamesDisplay.tsx b/ui/src/compoonents/ConfigFileNamesDisplay.tsx new file mode 100644 index 0000000..348f39f --- /dev/null +++ b/ui/src/compoonents/ConfigFileNamesDisplay.tsx @@ -0,0 +1,102 @@ +import { + createSignal, + createResource, + Component, + ErrorBoundary, + For, + Match, + Switch, +} from "solid-js"; +import { IndexApiVersion } from "../util/constants.jsx"; + +const ConfigFileNamesDisplay: Component<{ + version: IndexApiVersion; + class?: string; +}> = (props) => { + const [shouldFetch, setShouldFetch] = createSignal(false); + + const url = () => `api/${props.version}/index/info/config/names`; + + async function fetchNames(): Promise<{ status: number; body: string[] }> { + const response = await fetch(url()); + return { status: response.status, body: await response.json() }; + } + + const [configNamesResource, { refetch }] = createResource(shouldFetch, async () => { + if (!shouldFetch()) return null; + return await fetchNames(); + }); + + const handleFetch = () => { + if (shouldFetch()) { + refetch(); + } else { + setShouldFetch(true); + } + }; + + const isLoading = () => configNamesResource.loading; + + return ( +
+
+ +
+ + {/* Results Display */} +
+ + Unhandled state
}> + +
Loading config names...
+
+ + +
+ Error loading config names: {String(configNamesResource.error)} +
+
+ + +
+
+ Found {configNamesResource()!.body.length} config names: +
+
+ + {(item) => ( +
+ {item} +
+ )} +
+
+
+
+ + +
+ Click "Load Config Names" to fetch configuration names +
+
+ + +
+
+ ); +}; + +export default ConfigFileNamesDisplay; \ No newline at end of file diff --git a/ui/src/compoonents/ConfigPaths.tsx b/ui/src/compoonents/ConfigPaths.tsx new file mode 100644 index 0000000..69097b4 --- /dev/null +++ b/ui/src/compoonents/ConfigPaths.tsx @@ -0,0 +1,93 @@ +import { createSignal, createResource, For, type Component, Show, Match, Switch } from "solid-js"; +import { IndexApiVersion } from "../util/constants.jsx"; + +const ConfigPathsForm: Component<{ + class?: string; + version: IndexApiVersion; +}> = (props) => { + const [shouldFetch, setShouldFetch] = createSignal(false); + + const url = () => `api/${props.version}/index/info/config/paths`; + + async function fetchPaths(): Promise { + const response = await fetch(url()); + const data = await response.json(); + return data; + } + + const [pathsResource, { refetch }] = createResource(shouldFetch, async () => { + if (!shouldFetch()) return null; + return await fetchPaths(); + }); + + const handleFetch = () => { + if (shouldFetch()) { + refetch(); + } else { + setShouldFetch(true); + } + }; + + const isLoading = () => pathsResource.loading; + + return ( +
+
+ +
+ + {/* Results Display */} +
+ + +
Loading paths...
+
+ + +
+ Error loading paths: {String(pathsResource.error)} +
+
+ + +
+
+ Found {pathsResource()!.length} paths: +
+
+ + {(path) => ( +
+ {path} +
+ )} +
+
+
+
+ + +
+ Click "Load All Paths" to fetch configuration paths +
+
+
+
+
+ ); +}; + +export default ConfigPathsForm; \ No newline at end of file diff --git a/ui/src/compoonents/ConfigPathsByFile.tsx b/ui/src/compoonents/ConfigPathsByFile.tsx new file mode 100644 index 0000000..d187261 --- /dev/null +++ b/ui/src/compoonents/ConfigPathsByFile.tsx @@ -0,0 +1,103 @@ +import { createSignal, createResource, For, type Component, Match, Switch } from "solid-js"; +import { IndexApiVersion } from "../util/constants.jsx"; +import Toggleable from "./Toggleable.jsx"; + +const ConfigPathsByFile: Component<{ + class?: string; + version: IndexApiVersion; +}> = (props) => { + const [shouldFetch, setShouldFetch] = createSignal(false); + + const url = () => `api/${props.version}/index/info/config/map`; + + async function fetchConfigMap(): Promise> { + const response = await fetch(url()); + const data = await response.json(); + return data; + } + + const [configMapResource, { refetch }] = createResource(shouldFetch, async () => { + if (!shouldFetch()) return null; + return await fetchConfigMap(); + }); + + const handleFetch = () => { + if (shouldFetch()) { + refetch(); + } else { + setShouldFetch(true); + } + }; + + const isLoading = () => configMapResource.loading; + + return ( +
+
+ +
+ +
+ + +
Loading config map...
+
+ + +
+ Error loading config map: {String(configMapResource.error)} +
+
+ + +
+
+ Found {Object.keys(configMapResource()!).length} config files: +
+
+ + {([filename, paths]) => ( +
+ +
+ + {(path) => ( +
+ {path} +
+ )} +
+
+
+
+ )} +
+
+
+
+ + +
+ Click "Load Config Map" to fetch configuration mapping +
+
+
+
+
+ ); +}; + +export default ConfigPathsByFile; \ No newline at end of file diff --git a/ui/src/compoonents/ConfigSearchForm.tsx b/ui/src/compoonents/ConfigSearchForm.tsx new file mode 100644 index 0000000..3c07105 --- /dev/null +++ b/ui/src/compoonents/ConfigSearchForm.tsx @@ -0,0 +1,81 @@ +import { createSignal, createResource, type Component } from "solid-js"; +import SearchResult from "./SearchResult.jsx"; +import { IndexApiVersion } from "../util/constants.jsx"; +import { SearchResultData } from "../util/searchState.jsx"; + +const ConfigSearchForm: Component<{ + class?: string; + version: IndexApiVersion; +}> = (props) => { + const [configFileName, setConfigFileName] = createSignal(""); + const [queryPath, setQueryPath] = createSignal(""); + const [shouldSearch, setShouldSearch] = createSignal(false); + + const url = () => `api/${props.version}/index/config/${configFileName()}`; + + async function fetchProperties(): Promise { + const fullUrl = `${url()}?path=${queryPath()}`; + const response = await fetch(fullUrl); + const text = await response.text(); + return { status: response.status, text: text }; + } + + const [searchResource, { refetch }] = createResource( + shouldSearch, + async () => { + if (!shouldSearch()) return null; + return await fetchProperties(); + } + ); + + const handleSearch = () => { + if (!queryPath().trim() || !configFileName().trim()) { + return; + } + + shouldSearch() ? refetch() : setShouldSearch(true); + }; + + const isLoading = () => searchResource.loading; + + return ( +
+
+ { + setConfigFileName(e.target.value); + }} + /> + { + setQueryPath(e.target.value); + }} + /> + +
+ + +
+ ); +}; + +export default ConfigSearchForm; diff --git a/ui/src/compoonents/D3.tsx b/ui/src/compoonents/D3.tsx deleted file mode 100644 index 9aaddb8..0000000 --- a/ui/src/compoonents/D3.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as d3 from "d3"; -import { createSignal, For } from "solid-js"; - -type ChartProps> = { - width: number; - height: number; - margin: number; - data: T[]; - value: (d: T) => number; -}; - -const D3 = >(p: ChartProps) => { - const pieGenerator = d3.pie().value(p.value); - const parsedData = pieGenerator(p.data); - - const radius = Math.min(p.width, p.height) / 2 - p.margin; - - const arcGenerator = d3 - .arc>() - .innerRadius(0) - .outerRadius(radius); - - const colorGenerator = d3 - .scaleSequential(d3.interpolateWarm) - .domain([0, p.data.length]); - - const arcs = parsedData.map((d, i) => ({ - path: arcGenerator(d), - data: d.data, - color: colorGenerator(i), - })); - - return ( -
- D3 Test - - {/* */} - - {(d) => ( - - )} - - {/* */} - -
- ); -}; - -export default D3; diff --git a/ui/src/compoonents/Main.tsx b/ui/src/compoonents/Main.tsx index 8d7b27f..1a3caf1 100644 --- a/ui/src/compoonents/Main.tsx +++ b/ui/src/compoonents/Main.tsx @@ -1,14 +1,71 @@ import { type Component } from "solid-js"; -import Visualizer from "./Visualizer.jsx"; -import D3 from "./D3.jsx"; +import ConfigSearchForm from "./ConfigSearchForm.jsx"; +import { IndexApiVersion } from "../util/constants.jsx"; +import Toggleable from "./Toggleable.jsx"; +import ConfigFileNamesDisplay from "./ConfigFileNamesDisplay.jsx"; +import ConfigPathsForm from "./ConfigPaths.jsx"; +import ConfigPathsByFile from "./ConfigPathsByFile.jsx"; +import SearchNamesForm from "./SearchNamesForm.jsx"; +import StatsForm from "./StatsForm.jsx"; const Main: Component = () => { - return ( -
- {/* x.a}/> */} -
- +
+
+

V1

+

V2

+
+

index/info/config/names

+ + + + + + +
+

info/stats

+ + + + + + +
+

+ index/config/fileName?path=query +

+ + + + + + +
+

index/info/config/paths

+ + + + + + +
+

index/info/config/map

+ + + + + + +
+

+ info/search/names?tradeable=query +

+ + + + + +
); diff --git a/ui/src/compoonents/Search.tsx b/ui/src/compoonents/Search.tsx deleted file mode 100644 index b42e5f1..0000000 --- a/ui/src/compoonents/Search.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { - createResource, - createSignal, - ErrorBoundary, - Match, - Switch, - type Component, -} from "solid-js"; -import { getErrorMessage } from "../util/error.jsx"; - -const Search: Component<{ name: string; url: string; class?: string }> = ( - props -) => { - const [inputValue, setInputValue] = createSignal(""); - const [searchKey, setSearchKey] = createSignal(true); - const [searchQuery, setSearchQuery] = createSignal(null); - - async function fetchProperties( - query: string | null - ): Promise<{ status: number; text: string } | null> { - if (query === null) return null; - const url = `${props.url}${query}`; - const awaited = await fetch(url); - const text = await awaited.text(); - return { status: awaited.status, text: text }; - } - - const [getKey, { refetch }] = createResource(searchQuery, fetchProperties); - const handleSearch = () => { - const lastSearch = searchQuery(); - setSearchQuery(inputValue()); - if (lastSearch == searchQuery()) refetch(); - }; - - return ( - -
-

{props.name}

-
- { - setInputValue(e.target.value); - }} - > - -
- Unhandled state {getKey.state}
}> - - Search something - - Loading... - -
{getErrorMessage(getKey.error)}
-
- - Error {getKey()?.status} getting result
} - > - Not Found - - {`${getKey()?.text}`} - - - - -
-
-
- - ); -}; - -export default Search; diff --git a/ui/src/compoonents/SearchNamesForm.tsx b/ui/src/compoonents/SearchNamesForm.tsx new file mode 100644 index 0000000..7c83539 --- /dev/null +++ b/ui/src/compoonents/SearchNamesForm.tsx @@ -0,0 +1,121 @@ +import { + createSignal, + createResource, + For, + type Component, + Match, + Switch, +} from "solid-js"; +import { IndexApiVersion } from "../util/constants.jsx"; + +const SearchNamesForm: Component<{ + class?: string; + version: IndexApiVersion; +}> = (props) => { + const [shouldFetch, setShouldFetch] = createSignal(false); + const [tradeable, setTradeable] = createSignal(false); + + const url = () => + `api/${props.version}/index/info/search/names?tradeable=${tradeable()}`; + + async function fetchSearchNames(): Promise { + const response = await fetch(url()); + const data = await response.json(); + return data; + } + + const [searchNamesResource, { refetch }] = createResource( + shouldFetch, + async () => { + if (!shouldFetch()) return null; + return await fetchSearchNames(); + } + ); + + const handleFetch = () => { + if (shouldFetch()) { + refetch(); + } else { + setShouldFetch(true); + } + }; + + const isLoading = () => searchNamesResource.loading; + + return ( +
+
+
+ setTradeable(e.target.checked)} + class="w-4 h-4" + /> + +
+ + +
+ + {/* Results Display */} +
+ + +
Loading search names...
+
+ + +
+ Error loading search names: {String(searchNamesResource.error)} +
+
+ + +
+
+ Found {searchNamesResource()!.length}{" "} + {tradeable() ? "tradeable" : ""} search names: +
+
+ + {(name) => ( +
+ {name} +
+ )} +
+
+
+
+ + +
+ Click "Load Search Names" to fetch entry names +
+
+
+
+
+ ); +}; + +export default SearchNamesForm; diff --git a/ui/src/compoonents/SearchResult.tsx b/ui/src/compoonents/SearchResult.tsx new file mode 100644 index 0000000..647373d --- /dev/null +++ b/ui/src/compoonents/SearchResult.tsx @@ -0,0 +1,75 @@ +import { ErrorBoundary, Match, Switch, type Component, type Resource } from "solid-js"; +import { getErrorMessage } from "../util/error.jsx"; +import { ConfigEntry } from "../util/configEntry.jsx"; +import { SearchResultData } from "../util/searchState.jsx"; +import SearchResultDisplayV1 from "./SearchResultDisplayV1.jsx"; +import SearchResultDisplayV2 from "./SearchResultDisplayV2.jsx"; +import { IndexApiVersion } from "../util/constants.jsx"; + +interface SearchResultProps { + searchResource: Resource; + class?: string; + version: IndexApiVersion; +} + +const SearchResult: Component = (props) => { + function parseConfigResponse(response: SearchResultData): ConfigEntry | null { + if (!response || response.status !== 200) { + return null; + } + try { + return JSON.parse(response.text) as ConfigEntry; + } catch (error) { + console.error("Failed to parse config response:", error); + return null; + } + } + + const parsedData = () => { + const data = props.searchResource(); + return data ? parseConfigResponse(data) : null; + }; + + return ( + +
+ Unhandled state
}> + Loading... + + +
+ {getErrorMessage(props.searchResource.error)} +
+
+ + + + Error {props.searchResource()?.status} getting result + + } + > + + Not Found + + + {props.version === IndexApiVersion.v1 ? ( + + ) : ( + + )} + + + + + +
+ ); +}; + +export default SearchResult; \ No newline at end of file diff --git a/ui/src/compoonents/SearchResultDisplayV1.tsx b/ui/src/compoonents/SearchResultDisplayV1.tsx new file mode 100644 index 0000000..a81b93c --- /dev/null +++ b/ui/src/compoonents/SearchResultDisplayV1.tsx @@ -0,0 +1,58 @@ +import { createMemo, Show, type Component } from "solid-js"; +import { ConfigEntry } from "../util/configEntry.jsx"; +import { SearchResultData } from "../util/searchState.jsx"; + +interface SearchResultDisplayV1Props { + data: SearchResultData; + class?: string; +} + +const SearchResultDisplayV1: Component = ( + props +) => { + const parsedData = createMemo(() => { + if (!props.data || props.data.status !== 200) return null; + try { + return JSON.parse(props.data.text) as ConfigEntry; + } catch (error) { + console.error("Failed to parse V1 config response:", error); + return null; + } + }); + + return ( +
+

Config Entry (V1)

+ Failed to parse config entry}> + {(data) => ( + <> +
+ Name: + {data().name || "No name"} +
+
+ Source Config: {data().sourceConfig} +
+
+ Implementation: + {data().implementationType} +
+
+ Derived Implementation: {data().derivedImplementationType} +
+ +
+ Path: {data().path?.path || "No path"} +
+
+ Derived Path:{" "} + {data().derivedPath?.path || "No derived path"} +
+ + )} +
+
+ ); +}; + +export default SearchResultDisplayV1; diff --git a/ui/src/compoonents/SearchResultDisplayV2.tsx b/ui/src/compoonents/SearchResultDisplayV2.tsx new file mode 100644 index 0000000..0f8f3e7 --- /dev/null +++ b/ui/src/compoonents/SearchResultDisplayV2.tsx @@ -0,0 +1,82 @@ +import { createMemo, Show, type Component } from "solid-js"; +import { SearchResultData } from "../util/searchState.jsx"; +import { NewConfigEntry } from "../util/newConfigEntry.jsx"; +import TreeViewer, { GraphNode, HoverableGraphNode } from "./TreeViewer.jsx"; + +interface SearchResultDisplayV2Props { + data: SearchResultData; + class?: string; +} + +const SearchResultDisplayV2: Component = ( + props +) => { + const parsedData = createMemo(() => { + if (!props.data || props.data.status !== 200) return null; + try { + return JSON.parse(props.data.text) as NewConfigEntry; + } catch (error) { + console.error("Failed to parse V2 config response:", error); + return null; + } + }); + + const treeViewOfData = createMemo(() => { + if (parsedData() == null) { + return undefined; + } + + let leafEntryNode = parsedData()!; + let rootGraphNode: HoverableGraphNode = { + path: leafEntryNode.path.path!, + children: [], + isHovered: false, + }; + while (leafEntryNode.parentReference != null) { + leafEntryNode = leafEntryNode.parentReference.referencedEntry; + rootGraphNode = { + path: leafEntryNode.path.path!, + children: [rootGraphNode], + isHovered: false, + }; + } + return [rootGraphNode]; + }); + + return ( +
+

New Config Entry (V2)

+ Failed to parse new config entry} + > + {(data) => ( + <> +
+ Name: {data().name || "No name"} +
+
+ Config File: {data().configFileName} +
+
+ Implementation: {data().implementationType} +
+
+ Root Implementation: {data().rootImplementationType} +
+
+ Has Parent: {data().parentReference ? "Yes" : "No"} +
+ +
+ Path: {data().path?.path || "No path"} +
+ + + )} +
+
+ ); +}; + +export default SearchResultDisplayV2; diff --git a/ui/src/compoonents/StatsForm.tsx b/ui/src/compoonents/StatsForm.tsx new file mode 100644 index 0000000..1d429ef --- /dev/null +++ b/ui/src/compoonents/StatsForm.tsx @@ -0,0 +1,91 @@ +import { createSignal, createResource, For, type Component, Match, Switch } from "solid-js"; +import { IndexApiVersion } from "../util/constants.jsx"; + +const StatsForm: Component<{ + class?: string; + version: IndexApiVersion; +}> = (props) => { + const [shouldFetch, setShouldFetch] = createSignal(false); + + const url = () => `api/${props.version}/index/info/stats`; + + async function fetchStats(): Promise> { + const response = await fetch(url()); + const data = await response.json(); + return data; + } + + const [statsResource, { refetch }] = createResource(shouldFetch, async () => { + if (!shouldFetch()) return null; + return await fetchStats(); + }); + + const handleFetch = () => { + if (shouldFetch()) { + // If already fetched, refetch + refetch(); + } else { + // First time, trigger the fetch + setShouldFetch(true); + } + }; + + const isLoading = () => statsResource.loading; + + return ( +
+
+ +
+ + {/* Results Display */} +
+ + +
Loading stats...
+
+ + +
+ Error loading stats: {String(statsResource.error)} +
+
+ + +
+

Index Statistics

+
+ + {([statName, statValue]) => ( +
+ {statName}: {statValue.toLocaleString()} +
+ )} +
+
+
+
+ + +
+ Click "Load Stats" to fetch index statistics +
+
+
+
+
+ ); +}; + +export default StatsForm; \ No newline at end of file diff --git a/ui/src/compoonents/Toggleable.tsx b/ui/src/compoonents/Toggleable.tsx new file mode 100644 index 0000000..ec474c3 --- /dev/null +++ b/ui/src/compoonents/Toggleable.tsx @@ -0,0 +1,35 @@ +import { + Component, + JSX, + createSignal, + ParentComponent, + Show, + Switch, + Match, +} from "solid-js"; + +const Toggleable: Component<{ + children: JSX.Element; + label: string; + openByDefault?: boolean; +}> = (props) => { + const [isVisible, setIsVisible] = createSignal(props.openByDefault ?? false); + + return ( +
+ + {props.children} +
+ ); +}; + +export default Toggleable; diff --git a/ui/src/compoonents/TreeViewer.tsx b/ui/src/compoonents/TreeViewer.tsx new file mode 100644 index 0000000..a646240 --- /dev/null +++ b/ui/src/compoonents/TreeViewer.tsx @@ -0,0 +1,289 @@ +import { + createEffect, + createSignal, + For, + Show, + type Component, +} from "solid-js"; +import * as d3 from "d3"; + +export interface GraphNode { + path: string; + children: GraphNode[]; +} + +export interface HoverableGraphNode extends GraphNode { + isHovered: boolean; + children: HoverableGraphNode[]; +} + +const TreeViewer: Component<{ + data: HoverableGraphNode[] | undefined; + loading?: boolean; +}> = (props) => { + const firstElement = () => + props.data?.find((x) => x.children.some((y) => true)); + + const hierarchy = () => { + const root = firstElement(); + return !root ? null : d3.hierarchy(root, (x) => x.children); + }; + + const tree = () => { + const hierarchyRoot = hierarchy(); + if (!hierarchyRoot) return null; + const nodes = hierarchyRoot.count(); + const treeLayout = d3 + .tree() + .size([(nodes.value ?? 10) * 15, (nodes.value ?? 10) * 15]); + return treeLayout(hierarchyRoot); + }; + + // Helper function to collect all nodes recursively + const getAllNodes = () => { + const treeData = tree(); + if (!treeData) return []; + + const nodes: d3.HierarchyPointNode[] = []; + treeData.each((node) => { + nodes.push(node); + }); + return nodes; + }; + + const [transform, setTransform] = createSignal({ + x: 0, + y: 0, + scale: 1, + }); + const [isDragging, setIsDragging] = createSignal(false); + const [lastMousePos, setLastMousePos] = createSignal({ x: 0, y: 0 }); + + let canvasRef: HTMLCanvasElement | undefined; + + const [currentMousePos, setCurrentMousePos] = createSignal<{ + x: number; + y: number; + } | null>(null); + const [hoveredNode, setHoveredNode] = createSignal< + HoverableGraphNode | undefined + >(undefined); + + // Pan and zoom event handlers + const handleMouseDown = (e: MouseEvent) => { + setIsDragging(true); + setLastMousePos({ x: e.clientX, y: e.clientY }); + if (canvasRef) { + canvasRef.style.cursor = "grabbing"; + } + }; + + const handleMouseMove = (e: MouseEvent) => { + const rect = canvasRef!.getBoundingClientRect(); + setCurrentMousePos({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }); + + if (!isDragging()) return; + + const deltaX = e.clientX - lastMousePos().x; + const deltaY = e.clientY - lastMousePos().y; + + setTransform((prev) => ({ + ...prev, + x: prev.x + deltaX, + y: prev.y + deltaY, + })); + + setLastMousePos({ x: e.clientX, y: e.clientY }); + }; + + const handleMouseLeave = () => { + setHoveredNode(undefined); + if (canvasRef) { + canvasRef.style.cursor = "default"; + canvasRef.title = ""; + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + if (canvasRef) { + canvasRef.style.cursor = "grab"; + } + }; + + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + + const rect = canvasRef!.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1; + const newScale = Math.max( + 0.1, + Math.min(5, transform().scale * scaleFactor) + ); + + // Zoom towards mouse position + const scaleRatio = newScale / transform().scale; + const newX = mouseX - (mouseX - transform().x) * scaleRatio; + const newY = mouseY - (mouseY - transform().y) * scaleRatio; + + setTransform({ + x: newX, + y: newY, + scale: newScale, + }); + }; + + // Reset view function + const resetView = () => { + setTransform({ x: 200, y: 200, scale: 1 }); + }; + + // draw frame + createEffect(() => { + console.log("draw frame"); + if (canvasRef && tree()) { + const ctx = canvasRef.getContext("2d"); + if (ctx) { + const currentTransform = transform(); + + // Clear canvas + ctx.clearRect(0, 0, 400, 400); + + // Save context and apply transform + ctx.save(); + ctx.translate(currentTransform.x, currentTransform.y); + ctx.scale(currentTransform.scale, currentTransform.scale); + + // Draw nodes and links + const treeData = tree()!; + + // Draw links first + ctx.strokeStyle = "#999"; + ctx.lineWidth = 1 / currentTransform.scale; // Adjust line width for zoom + treeData.links().forEach((link) => { + ctx.beginPath(); + ctx.moveTo(link.source.x!, link.source.y!); + ctx.lineTo(link.target.x!, link.target.y!); + ctx.stroke(); + }); + + const mousePos = currentMousePos(); + let newHoveredNode: HoverableGraphNode | undefined = undefined; + // Calculate canvas coordinates once if we have mouse position + let canvasX: number, canvasY: number, radiusSquared: number; + if (mousePos) { + canvasX = (mousePos.x - currentTransform.x) / currentTransform.scale; + canvasY = (mousePos.y - currentTransform.y) / currentTransform.scale; + radiusSquared = Math.pow(8 / currentTransform.scale, 2); + } + // Draw nodes and hover detect + ctx.fillStyle = "#69b3a2"; + treeData.each((node) => { + ctx.fillStyle = "#69b3a2"; + ctx.beginPath(); + const nodeRadius = 5 / currentTransform.scale; // Adjust node size for zoom + ctx.arc(node.x!, node.y!, nodeRadius, 0, 2 * Math.PI); + + node.data.isHovered = false; + // Hit test during drawing + if (mousePos) { + const dx = canvasX - node.x!; + const dy = canvasY - node.y!; + if (dx * dx + dy * dy <= radiusSquared) { + node.data.isHovered = true; + newHoveredNode = node.data; + ctx.fillStyle = "#ffffff"; + } + } + + ctx.fill(); + }); + + // Restore context + ctx.restore(); + } + } + }); + + // Set up event listeners when canvas is ready + createEffect(() => { + if (canvasRef) { + canvasRef.style.cursor = "grab"; + + // Mouse events for panning + canvasRef.addEventListener("mousedown", handleMouseDown); + canvasRef.addEventListener("mousemove", handleMouseMove); + canvasRef.addEventListener("mouseup", handleMouseUp); + canvasRef.addEventListener("mouseleave", handleMouseLeave); + + // Wheel event for zooming + canvasRef.addEventListener("wheel", handleWheel); + + // Initialize centered view + setTransform({ x: 200, y: 200, scale: 1 }); + + // Cleanup function + return () => { + canvasRef?.removeEventListener("mousedown", handleMouseDown); + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + canvasRef?.removeEventListener("wheel", handleWheel); + }; + } + }); + + const showDebug = false; + + return ( +
+
+ +
+
+ +
+ Pan: Click and drag | Zoom: Mouse wheel | Scale:{" "} + {transform().scale.toFixed(2)}x +
+
+ +
+ Roots: {props.data?.length || 0} | Total: {getAllNodes().length} +
+ Root: x={tree()?.x}, y={tree()?.y} +
+ Transform: x={transform().x.toFixed(1)}, y= + {transform().y.toFixed(1)}, scale={transform().scale.toFixed(2)}, +
+ Mouse: x= {currentMousePos()?.x.toFixed(2)} y= + {currentMousePos()?.y.toFixed(2)} + + {(node) => ( +
+ Path: {node.data.path} - x: {node.x}, y: {node.y} +
+ )} +
+
+
+
+ ); +}; + +export default TreeViewer; diff --git a/ui/src/compoonents/Visualizer.tsx b/ui/src/compoonents/Visualizer.tsx index 23e3701..07e0773 100644 --- a/ui/src/compoonents/Visualizer.tsx +++ b/ui/src/compoonents/Visualizer.tsx @@ -1,23 +1,9 @@ import { - createEffect, createResource, createSignal, - For, - Show, type Component, } from "solid-js"; -import * as d3 from "d3"; - -interface GraphNode { - path: string; - children: GraphNode[]; -} -type GraphNodeResponse = GraphNode[]; - -interface HoverableGraphNode extends GraphNode { - isHovered: boolean; - children: HoverableGraphNode[]; -} +import TreeViewer, { type HoverableGraphNode } from "./TreeViewer.jsx"; const Visualizer: Component = () => { const [inputValue, setInputValue] = createSignal(""); @@ -30,6 +16,7 @@ const Visualizer: Component = () => { const awaited = await fetch(url); return { status: awaited.status, body: await awaited.json() }; } + const [response, { refetch }] = createResource(searchQuery, fetchProperties); const handleSearch = () => { @@ -38,278 +25,21 @@ const Visualizer: Component = () => { if (lastSearch === searchQuery()) refetch(); }; - const firstElement = () => - response()?.body.find((x) => x.children.some((y) => true)); - - const hierarchy = () => { - const root = firstElement(); - return !root ? null : d3.hierarchy(root, (x) => x.children); - }; - - const tree = () => { - const hierarchyRoot = hierarchy(); - if (!hierarchyRoot) return null; - const nodes = hierarchyRoot.count(); - const treeLayout = d3 - .tree() - .size([(nodes.value ?? 10) * 15, (nodes.value ?? 10) * 15]); - return treeLayout(hierarchyRoot); - }; - - // Helper function to collect all nodes recursively - const getAllNodes = () => { - const treeData = tree(); - if (!treeData) return []; - - const nodes: d3.HierarchyPointNode[] = []; - treeData.each((node) => { - nodes.push(node); - }); - return nodes; - }; - - const [transform, setTransform] = createSignal({ - x: 0, - y: 0, - scale: 1, - }); - const [isDragging, setIsDragging] = createSignal(false); - const [lastMousePos, setLastMousePos] = createSignal({ x: 0, y: 0 }); - - let canvasRef: HTMLCanvasElement | undefined; - - const [currentMousePos, setCurrentMousePos] = createSignal<{ - x: number; - y: number; - } | null>(null); - const [hoveredNode, setHoveredNode] = createSignal< - HoverableGraphNode | undefined - >(undefined); - - // Pan and zoom event handlers - const handleMouseDown = (e: MouseEvent) => { - setIsDragging(true); - setLastMousePos({ x: e.clientX, y: e.clientY }); - if (canvasRef) { - canvasRef.style.cursor = "grabbing"; - } - }; - - const handleMouseMove = (e: MouseEvent) => { - const rect = canvasRef!.getBoundingClientRect(); - setCurrentMousePos({ - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - - if (!isDragging()) return; - - const deltaX = e.clientX - lastMousePos().x; - const deltaY = e.clientY - lastMousePos().y; - - setTransform((prev) => ({ - ...prev, - x: prev.x + deltaX, - y: prev.y + deltaY, - })); - - setLastMousePos({ x: e.clientX, y: e.clientY }); - }; - - const handleMouseLeave = () => { - setHoveredNode(undefined); - if (canvasRef) { - canvasRef.style.cursor = "default"; - canvasRef.title = ""; - } - }; - - const handleMouseUp = () => { - setIsDragging(false); - if (canvasRef) { - canvasRef.style.cursor = "grab"; - } - }; - - const handleWheel = (e: WheelEvent) => { - e.preventDefault(); - - const rect = canvasRef!.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - - const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1; - const newScale = Math.max( - 0.1, - Math.min(5, transform().scale * scaleFactor) - ); - - // Zoom towards mouse position - const scaleRatio = newScale / transform().scale; - const newX = mouseX - (mouseX - transform().x) * scaleRatio; - const newY = mouseY - (mouseY - transform().y) * scaleRatio; - - setTransform({ - x: newX, - y: newY, - scale: newScale, - }); - }; - - // Reset view function - const resetView = () => { - setTransform({ x: 200, y: 200, scale: 1 }); - }; - - // draw frame - createEffect(() => { - console.log("draw frame"); - if (canvasRef && tree()) { - const ctx = canvasRef.getContext("2d"); - if (ctx) { - const currentTransform = transform(); - - // Clear canvas - ctx.clearRect(0, 0, 400, 400); - - // Save context and apply transform - ctx.save(); - ctx.translate(currentTransform.x, currentTransform.y); - ctx.scale(currentTransform.scale, currentTransform.scale); - - // Draw nodes and links - const treeData = tree()!; - - // Draw links first - ctx.strokeStyle = "#999"; - ctx.lineWidth = 1 / currentTransform.scale; // Adjust line width for zoom - treeData.links().forEach((link) => { - ctx.beginPath(); - ctx.moveTo(link.source.x!, link.source.y!); - ctx.lineTo(link.target.x!, link.target.y!); - ctx.stroke(); - }); - - const mousePos = currentMousePos(); - let newHoveredNode: HoverableGraphNode | undefined = undefined; - // Calculate canvas coordinates once if we have mouse position - let canvasX: number, canvasY: number, radiusSquared: number; - if (mousePos) { - canvasX = (mousePos.x - currentTransform.x) / currentTransform.scale; - canvasY = (mousePos.y - currentTransform.y) / currentTransform.scale; - radiusSquared = Math.pow(8 / currentTransform.scale, 2); - } - // Draw nodes and hover detect - ctx.fillStyle = "#69b3a2"; - treeData.each((node) => { - ctx.fillStyle = "#69b3a2"; - ctx.beginPath(); - const nodeRadius = 5 / currentTransform.scale; // Adjust node size for zoom - ctx.arc(node.x!, node.y!, nodeRadius, 0, 2 * Math.PI); - - node.data.isHovered = false; - // Hit test during drawing - if (mousePos) { - const dx = canvasX - node.x!; - const dy = canvasY - node.y!; - if (dx * dx + dy * dy <= radiusSquared) { - node.data.isHovered = true; - newHoveredNode = node.data; - ctx.fillStyle = "#ffffff"; - } - } - - ctx.fill(); - }); - - // Restore context - ctx.restore(); - } - } - }); - - // Set up event listeners when canvas is ready - createEffect(() => { - if (canvasRef) { - canvasRef.style.cursor = "grab"; - - // Mouse events for panning - canvasRef.addEventListener("mousedown", handleMouseDown); - canvasRef.addEventListener("mousemove", handleMouseMove); - canvasRef.addEventListener("mouseup", handleMouseUp); - canvasRef.addEventListener("mouseleave", handleMouseLeave); - - // Wheel event for zooming - canvasRef.addEventListener("wheel", handleWheel); - - // Initialize centered view - setTransform({ x: 200, y: 200, scale: 1 }); - - // Cleanup function - return () => { - canvasRef?.removeEventListener("mousedown", handleMouseDown); - window.removeEventListener("mousemove", handleMouseMove); - window.removeEventListener("mouseup", handleMouseUp); - canvasRef?.removeEventListener("wheel", handleWheel); - }; - } - }); - - const showDebug = false; return (

Properties

-
-
+
- -
-
- -
- Pan: Click and drag | Zoom: Mouse wheel | Scale:{" "} - {transform().scale.toFixed(2)}x -
-
- -
- Roots: {response()?.body?.length || 0} | Total: {getAllNodes().length} -
- Root: x={tree()?.x}, y={tree()?.y} -
- Transform: x={transform().x.toFixed(1)}, y={transform().y.toFixed(1)}, - scale={transform().scale.toFixed(2)}, -
- Mouse: x= {currentMousePos()?.x.toFixed(2)} y= - {currentMousePos()?.y.toFixed(2)} - - {(node) => ( -
- Path: {node.data.path} - x: {node.x}, y: {node.y} -
- )} -
-
-
+
); }; -export default Visualizer; +export default Visualizer; \ No newline at end of file diff --git a/ui/src/util/configEntry.tsx b/ui/src/util/configEntry.tsx new file mode 100644 index 0000000..c9bb669 --- /dev/null +++ b/ui/src/util/configEntry.tsx @@ -0,0 +1,18 @@ +import { ParameterArray, Path } from "./parameterArray.jsx"; + +export interface ConfigEntry { + implementationType: string; + derivedImplementationType: string; + path: Path; + derivedPath: Path; + sourceConfig: string; + parameters: ParameterArray; + routedParameters: ParameterArray; + derivedParameters: ParameterArray; + + // Computed properties that would come from getters + effectiveImplementation?: string; + name?: string | null; + derived?: boolean; +} + diff --git a/ui/src/util/constants.tsx b/ui/src/util/constants.tsx new file mode 100644 index 0000000..8fc2ce7 --- /dev/null +++ b/ui/src/util/constants.tsx @@ -0,0 +1,4 @@ +export enum IndexApiVersion { + v1 = "v1", + v2 = "v2", +}; \ No newline at end of file diff --git a/ui/src/util/newConfigEntry.tsx b/ui/src/util/newConfigEntry.tsx new file mode 100644 index 0000000..1a80262 --- /dev/null +++ b/ui/src/util/newConfigEntry.tsx @@ -0,0 +1,19 @@ +import { ParameterArray, Path } from "./parameterArray.jsx"; + +export interface NewConfigEntryReference { + referencedEntry: NewConfigEntry; +} + +export interface NewConfigEntry { + configFileName: string; + path: Path; + implementationType: string; + parentReference: NewConfigEntryReference | null; + entryParameters: ParameterArray; + routedParameters: ParameterArray; + + // Computed properties that would come from getters + rootImplementationType?: string; + effectiveParameters?: ParameterArray; + name?: string | null; +} diff --git a/ui/src/util/parameterArray.tsx b/ui/src/util/parameterArray.tsx new file mode 100644 index 0000000..78aed73 --- /dev/null +++ b/ui/src/util/parameterArray.tsx @@ -0,0 +1,12 @@ +export interface ParameterArray { + [key: string]: ParameterValue; +} + +export interface ParameterValue { + value: any; + nested: boolean; +} + +export interface Path { + path: string | null; +} diff --git a/ui/src/util/searchState.tsx b/ui/src/util/searchState.tsx new file mode 100644 index 0000000..592fd91 --- /dev/null +++ b/ui/src/util/searchState.tsx @@ -0,0 +1,11 @@ + +export type SearchResultData = { + status: number; + text: string; +} | null; + +export type SearchState = + | { state: 'idle' } + | { state: 'loading' } + | { state: 'success'; data: TData } + | { state: 'error'; error: TError }; From 4bdf115fbeb42f65f58b6136758433296d73fbe9 Mon Sep 17 00:00:00 2001 From: nichita Date: Sun, 7 Sep 2025 14:05:47 +0200 Subject: [PATCH 4/9] fix bug where effective parameters were not taking into consideration parameters defined on the reference --- .../crowfunder/cogmaster/Parseable/NewConfigEntry.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java index 85a3599..21c6bdd 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/NewConfigEntry.java @@ -46,9 +46,11 @@ public ParameterArray getEffectiveParameters() { if (parentReference == null) return entryParameters; - // accumulate params, entryParams take priority over the parentReference's - // params - return entryParameters.derive(parentReference.referencedEntry.getEffectiveParameters()); + // accumulate params: + // parent's effective parameters get overwritten by any parameters included in the child reference + // resulting parameters get overwritten by any parameters directly contained in the child + // Note: as of now, most of the time the parameters are defined in the reference + return entryParameters.derive(parentReference.getParameters()).derive(parentReference.referencedEntry.getEffectiveParameters()); } // Return effective name using routes From e2dda7dd36ea9cfd816afed029e95c00eec8c7e6 Mon Sep 17 00:00:00 2001 From: nichita Date: Sun, 7 Sep 2025 14:08:16 +0200 Subject: [PATCH 5/9] small ui bug: says result is tradeable names even when not --- ui/src/compoonents/ConfigSearchForm.tsx | 6 +++--- ui/src/compoonents/Main.tsx | 4 ++-- ui/src/compoonents/SearchNamesForm.tsx | 19 ++++++++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/ui/src/compoonents/ConfigSearchForm.tsx b/ui/src/compoonents/ConfigSearchForm.tsx index 3c07105..418884e 100644 --- a/ui/src/compoonents/ConfigSearchForm.tsx +++ b/ui/src/compoonents/ConfigSearchForm.tsx @@ -39,10 +39,10 @@ const ConfigSearchForm: Component<{ const isLoading = () => searchResource.loading; return ( -
+
{ return (
-

V1

-

V2

+

V1

+

V2


index/info/config/names

diff --git a/ui/src/compoonents/SearchNamesForm.tsx b/ui/src/compoonents/SearchNamesForm.tsx index 7c83539..5e21809 100644 --- a/ui/src/compoonents/SearchNamesForm.tsx +++ b/ui/src/compoonents/SearchNamesForm.tsx @@ -8,6 +8,11 @@ import { } from "solid-js"; import { IndexApiVersion } from "../util/constants.jsx"; +interface SearchResult { + names: string[]; + wasTradeableOnly: boolean; +} + const SearchNamesForm: Component<{ class?: string; version: IndexApiVersion; @@ -18,10 +23,14 @@ const SearchNamesForm: Component<{ const url = () => `api/${props.version}/index/info/search/names?tradeable=${tradeable()}`; - async function fetchSearchNames(): Promise { + async function fetchSearchNames(): Promise { + const tradeableAtFetchTime = tradeable(); // Capture the value at fetch time const response = await fetch(url()); const data = await response.json(); - return data; + return { + names: data, + wasTradeableOnly: tradeableAtFetchTime + }; } const [searchNamesResource, { refetch }] = createResource( @@ -92,11 +101,11 @@ const SearchNamesForm: Component<{
- Found {searchNamesResource()!.length}{" "} - {tradeable() ? "tradeable" : ""} search names: + Found {searchNamesResource()!.names.length}{" "} + {searchNamesResource()!.wasTradeableOnly ? "tradeable" : ""} search names:
- + {(name) => (
{name} From 9314ce737876b989f5bec153ccb57f32ba61638e Mon Sep 17 00:00:00 2001 From: nichita Date: Sun, 7 Sep 2025 15:03:02 +0200 Subject: [PATCH 6/9] add in all the data needed for the current tests to pass --- api/src/test/resources/parseable/actor.xml | 132 ++++++++++++++++++ api/src/test/resources/parseable/item.xml | 25 ++++ .../resources/routers/ItemConfig/Sword.yml | 9 ++ 3 files changed, 166 insertions(+) create mode 100644 api/src/test/resources/routers/ItemConfig/Sword.yml diff --git a/api/src/test/resources/parseable/actor.xml b/api/src/test/resources/parseable/actor.xml index 066879f..7ce3f75 100644 --- a/api/src/test/resources/parseable/actor.xml +++ b/api/src/test/resources/parseable/actor.xml @@ -12,5 +12,137 @@ + + + Block/Parts/Barbed Hedgehog Base + + + + Variant + + + Damage Type + implementation.handlers[0].action.damage.type + + + Varient + implementation.sprite.model["File"], implementation.sprite.destruction_transient["Model"]["File"] + + + + + + Normal + + Damage Type + NORMAL + Varient + world/dynamic/barbed_hedgehog/barbwire.png + + + + + Elemental + + Damage Type + ELEMENTAL + Varient + world/dynamic/barbed_hedgehog/barbwire_elemental.png + + + + + Piercing + + Damage Type + PIERCING + Varient + world/dynamic/barbed_hedgehog/barbwire_piercing.png + + + + + Shadow + + Damage Type + SHADOW + Varient + world/dynamic/barbed_hedgehog/barbwire_shadow.png + + + + Normal + + + + + 3 + + world/dynamic/barbed_hedgehog/model.dat + + File + world/dynamic/barbed_hedgehog/barbwire.png + + + + particle/fx_generic_destruct.dat + + Model + + world/dynamic/barbed_hedgehog/model.dat + + File + world/dynamic/barbed_hedgehog/barbwire.png + + + Model Transform + + + + + + model/scripted/transient_sound.dat + + Loop Duration + 0.0 + Move with Origin + false + Sounder + + Custom/on_Hit/Metal + + Pitch shift + Default + + + Transform + + + Transient + + particle/fx_gethit_block.dat + + + + + + 0.5 + + + + + + + Monster/Damage/Attack Base -05 + + 8 + + + + + 0 + 2 + +
diff --git a/api/src/test/resources/parseable/item.xml b/api/src/test/resources/parseable/item.xml index 9888303..718941d 100644 --- a/api/src/test/resources/parseable/item.xml +++ b/api/src/test/resources/parseable/item.xml @@ -31,6 +31,31 @@ + + + + Accessory/Parts/Base, Custom Colors + + + Icon + implementation.icon.file + + + Accessory + implementation.accessory + + + Colorizations + implementation.icon.colorizations + + + + + ui/icon/inventory/icon_accessory-afront.png + + true + -1 + Weapon/Sword/Brandish diff --git a/api/src/test/resources/routers/ItemConfig/Sword.yml b/api/src/test/resources/routers/ItemConfig/Sword.yml new file mode 100644 index 0000000..4cceafa --- /dev/null +++ b/api/src/test/resources/routers/ItemConfig/Sword.yml @@ -0,0 +1,9 @@ +!!com.crowfunder.cogmaster.Routers.Router +implementation: "com.threerings.projectx.item.config.ItemConfig$Sword" +routes: + name: "Name" + flavor: "Flavor" + rarity: "Rarity" + model: "Model/name" + icon: "Icon/file" + itemProp: "Item Prop/name" \ No newline at end of file From 1f6e628e699c0cf526705c86924b85ec8b211a3d Mon Sep 17 00:00:00 2001 From: nichita Date: Mon, 8 Sep 2025 23:42:34 +0200 Subject: [PATCH 7/9] copy tests to v2, change parameters test to effectiveParameters --- .../Parseable/ParseableGraphController.java | 1 + .../IndexV2/IndexControllerV2Test.java | 93 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 api/src/test/java/com/crowfunder/cogmaster/IndexV2/IndexControllerV2Test.java diff --git a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java index 0d78244..ca7d8a2 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Parseable/ParseableGraphController.java @@ -20,6 +20,7 @@ public class GraphNode { public ArrayList children = new ArrayList<>(); } + // endpoint used to draw a graph of all our entries on local UI @GetMapping("all") public ResponseEntity> getAllconfigs() { var allNodesMap = new HashMap(); diff --git a/api/src/test/java/com/crowfunder/cogmaster/IndexV2/IndexControllerV2Test.java b/api/src/test/java/com/crowfunder/cogmaster/IndexV2/IndexControllerV2Test.java new file mode 100644 index 0000000..9c2a375 --- /dev/null +++ b/api/src/test/java/com/crowfunder/cogmaster/IndexV2/IndexControllerV2Test.java @@ -0,0 +1,93 @@ +package com.crowfunder.cogmaster.IndexV2; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class IndexControllerV2Test { + + @Autowired + private MockMvc mockMvc; + + @Test + void getConfigByPathDeepParameter() throws Exception { + String path = "Accessory/Armor/Aura/Snipe Aura, Cocoa"; + ResultActions result = mockMvc.perform(get("/api/v2/index/config/item?path={path}", path)); + var res = result.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.path.path").value(path)) + .andExpect(jsonPath("$.effectiveParameters.hashMap.Colorizations.value.hashMap.entry.value.[0].value.hashMap.source.value.hashMap.colorization.value").value("1295")); + } + + @Test + void getConfigByPath() throws Exception { + String path = "Block/Barbed Hedgehog"; + ResultActions result = mockMvc.perform(get("/api/v2/index/config/actor?path={path}", path)); + result.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.path.path").value(path)); + + // Path without forward slashes + path = "Action"; + result = mockMvc.perform(get("/api/v2/index/config/effect?path={path}", path)); + result.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.path.path").value(path)); + } + + @Test + void getConfigByPathFail() throws Exception { + + // Fake config + String path = "Block/Fake Entry That Does Not Exist"; + ResultActions result = mockMvc.perform(get("/api/v2/index/config/actor?path={path}", path)); + result.andExpect(status().isNotFound()); + + // Fake config without forward slashes + path = "Fake Entry That Does Not Exist"; + result = mockMvc.perform(get("/api/v2/index/config/actor?path={path}", path)); + result.andExpect(status().isNotFound()); + + // Bad path with backslashes + path = "Block\\Barbed Hedgehog"; + result = mockMvc.perform(get("/api/v2/index/config/actor?path={path}", path)); + result.andExpect(status().isNotFound()); + } + + // Config that does not exist + @Test + void getConfigFail() throws Exception { + String path = "Block/Fake Entry That Does Not Exist"; + String configName = "FakeConfigThatDoesNotExist"; + ResultActions result = mockMvc.perform(get("/api/v2/index/config/{configName}?path={path}", configName, path)); + result.andExpect(status().isNotFound()); + } + + @Test + void getConfigByName() throws Exception { + String path = "Brandish"; + ResultActions result = mockMvc.perform(get("/api/v2/index/search?q={path}", path)); + result.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.[0].path.path").value("Weapon/Sword/Brandish")); + } + + @Test + void getConfigByNameFail() throws Exception { + String path = "Bollocksnamethatdoesnotexist"; + ResultActions result = mockMvc.perform(get("/api/v2/index/search?q={path}", path)); + result.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isEmpty()); + } +} \ No newline at end of file From 9105e3c9d9f8d9871a6774fbe766477e46ad11d6 Mon Sep 17 00:00:00 2001 From: nichita Date: Tue, 9 Sep 2025 19:02:35 +0200 Subject: [PATCH 8/9] add basic clicking functionality to treeViewer --- ui/src/compoonents/TreeViewer.tsx | 94 ++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/ui/src/compoonents/TreeViewer.tsx b/ui/src/compoonents/TreeViewer.tsx index a646240..bd28f29 100644 --- a/ui/src/compoonents/TreeViewer.tsx +++ b/ui/src/compoonents/TreeViewer.tsx @@ -14,6 +14,7 @@ export interface GraphNode { export interface HoverableGraphNode extends GraphNode { isHovered: boolean; + isClicked: boolean; children: HoverableGraphNode[]; } @@ -68,12 +69,15 @@ const TreeViewer: Component<{ const [hoveredNode, setHoveredNode] = createSignal< HoverableGraphNode | undefined >(undefined); + const [clickedNode, setClickedNode] = createSignal< + HoverableGraphNode | undefined + >(undefined); // Pan and zoom event handlers const handleMouseDown = (e: MouseEvent) => { setIsDragging(true); setLastMousePos({ x: e.clientX, y: e.clientY }); - if (canvasRef) { + if (canvasRef && !hoveredNode()) { canvasRef.style.cursor = "grabbing"; } }; @@ -114,6 +118,16 @@ const TreeViewer: Component<{ } }; + // Add click handler + const handleClick = (e: MouseEvent) => { + if (isDragging()) return; // Don't handle clicks during drag + + if (hoveredNode()) { + hoveredNode()!.isClicked = true; + setClickedNode(hoveredNode())!.isClicked = true; + } + }; + const handleWheel = (e: WheelEvent) => { e.preventDefault(); @@ -146,7 +160,7 @@ const TreeViewer: Component<{ // draw frame createEffect(() => { - console.log("draw frame"); + if (showDebug) console.log("draw frame"); if (canvasRef && tree()) { const ctx = canvasRef.getContext("2d"); if (ctx) { @@ -174,7 +188,6 @@ const TreeViewer: Component<{ }); const mousePos = currentMousePos(); - let newHoveredNode: HoverableGraphNode | undefined = undefined; // Calculate canvas coordinates once if we have mouse position let canvasX: number, canvasY: number, radiusSquared: number; if (mousePos) { @@ -182,15 +195,16 @@ const TreeViewer: Component<{ canvasY = (mousePos.y - currentTransform.y) / currentTransform.scale; radiusSquared = Math.pow(8 / currentTransform.scale, 2); } + + let newHoveredNode: HoverableGraphNode | undefined = undefined; // Draw nodes and hover detect - ctx.fillStyle = "#69b3a2"; treeData.each((node) => { - ctx.fillStyle = "#69b3a2"; ctx.beginPath(); - const nodeRadius = 5 / currentTransform.scale; // Adjust node size for zoom + const nodeRadius = 6 / currentTransform.scale; // Adjust node size for zoom ctx.arc(node.x!, node.y!, nodeRadius, 0, 2 * Math.PI); node.data.isHovered = false; + if (clickedNode() != node.data) node.data.isClicked = false; // Hit test during drawing if (mousePos) { const dx = canvasX - node.x!; @@ -198,12 +212,21 @@ const TreeViewer: Component<{ if (dx * dx + dy * dy <= radiusSquared) { node.data.isHovered = true; newHoveredNode = node.data; - ctx.fillStyle = "#ffffff"; } } + // Determine fill color based on state + let fillColor = "#69b3a2"; // Default color + if (node.data.isClicked) { + fillColor = "#ff8c00"; // Orange for clicked + } else if (node.data.isHovered) { + fillColor = "#ffffff"; // White for hovered + } + + ctx.fillStyle = fillColor; ctx.fill(); }); + setHoveredNode(newHoveredNode); // Restore context ctx.restore(); @@ -221,6 +244,7 @@ const TreeViewer: Component<{ canvasRef.addEventListener("mousemove", handleMouseMove); canvasRef.addEventListener("mouseup", handleMouseUp); canvasRef.addEventListener("mouseleave", handleMouseLeave); + canvasRef.addEventListener("click", handleClick); // Wheel event for zooming canvasRef.addEventListener("wheel", handleWheel); @@ -233,6 +257,7 @@ const TreeViewer: Component<{ canvasRef?.removeEventListener("mousedown", handleMouseDown); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); + canvasRef?.removeEventListener("click", handleClick); canvasRef?.removeEventListener("wheel", handleWheel); }; } @@ -241,25 +266,27 @@ const TreeViewer: Component<{ const showDebug = false; return ( -
-
- -
-
- -
- Pan: Click and drag | Zoom: Mouse wheel | Scale:{" "} - {transform().scale.toFixed(2)}x +
+
+
+ +
+
+ +
+ Pan: Click and drag | Zoom: Mouse wheel | Click: Select node | + Scale: {transform().scale.toFixed(2)}x +
@@ -276,12 +303,25 @@ const TreeViewer: Component<{ {(node) => (
- Path: {node.data.path} - x: {node.x}, y: {node.y} + Path: {node.data.path} - x: {node.x}, y: {node.y} - Clicked:{" "} + {node.data.isClicked ? "Yes" : "No"}
)}
+
+ Hovered: + + {(node) => Path: {node().path}} + +
+
+ Clicked: + + {(node) => Path: {node().path}} + +
); }; From 9179650a13717c43bdeb765d06d149d133bb1033 Mon Sep 17 00:00:00 2001 From: nichita Date: Tue, 9 Sep 2025 19:12:34 +0200 Subject: [PATCH 9/9] Add deprecated attributes to v1 controller endpoints --- .../cogmaster/Index/IndexController.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/crowfunder/cogmaster/Index/IndexController.java b/api/src/main/java/com/crowfunder/cogmaster/Index/IndexController.java index e249325..68179ec 100644 --- a/api/src/main/java/com/crowfunder/cogmaster/Index/IndexController.java +++ b/api/src/main/java/com/crowfunder/cogmaster/Index/IndexController.java @@ -9,6 +9,10 @@ import java.util.Optional; import java.util.Set; +/** + * @deprecated use {@link IndexController2} instead. + */ +@Deprecated @RestController @RequestMapping("api/v1/index") public class IndexController { @@ -19,38 +23,63 @@ public IndexController(IndexService indexService) { this.indexService = indexService; } + /** + * @deprecated use {@link IndexController2#resolveConfigByPath(String, String)} + * instead. + */ + @Deprecated @GetMapping("config/{configName}") - public ResponseEntity resolveConfigByPath(@PathVariable("configName") String configName, @RequestParam String path) { + public ResponseEntity resolveConfigByPath(@PathVariable("configName") String configName, + @RequestParam String path) { Optional resolvedConfig = Optional.ofNullable(indexService.resolveConfig(configName, path)); return resolvedConfig.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } + /** + * @deprecated use {@link IndexController2#resolveConfigByName(String)} instead. + */ + @Deprecated @GetMapping("search") public ResponseEntity> resolveConfigByName(@RequestParam String q) { Optional> resolvedConfigs = Optional.ofNullable(indexService.resolveConfigByName(q)); return resolvedConfigs.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } + /** + * @deprecated use {@link IndexController2#getAllConfigNames()} instead. + */ + @Deprecated @GetMapping("info/config/names") public ResponseEntity> getAllConfigNames() { Optional> resolvedConfigs = Optional.ofNullable(indexService.getAllConfigNames()); return resolvedConfigs.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } + /** + * @deprecated use {@link IndexController2#getAllConfigPaths()} instead. + */ + @Deprecated @GetMapping("info/config/paths") public ResponseEntity> getAllConfigPaths() { Optional> resolvedConfigs = Optional.ofNullable(indexService.getAllConfigPaths()); return resolvedConfigs.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } + /** + * @deprecated use {@link IndexController2#getConfigPathsMap()} instead. + */ @GetMapping("info/config/map") public ResponseEntity>> getConfigPathsMap() { Optional>> resolvedConfigs = Optional.ofNullable(indexService.getConfigPathsMap()); return resolvedConfigs.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } + /** + * @deprecated use {@link IndexController2#getAllEntryNames(boolean)} instead. + */ @GetMapping("info/search/names") - public ResponseEntity> getAllEntryNames(@RequestParam(name= "tradeable", required = false, defaultValue = "false") boolean tradeable) { + public ResponseEntity> getAllEntryNames( + @RequestParam(name = "tradeable", required = false, defaultValue = "false") boolean tradeable) { Optional> resolvedConfigs; if (tradeable) { resolvedConfigs = Optional.ofNullable(indexService.getTradeableEntryNames()); @@ -60,6 +89,9 @@ public ResponseEntity> getAllEntryNames(@RequestParam(name= "tradeab return resolvedConfigs.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } + /** + * @deprecated use {@link IndexController2#getStats()} instead. + */ @GetMapping("info/stats") public ResponseEntity> getStats() { Optional> resolvedConfigs = Optional.ofNullable(indexService.getIndexStats());