├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── main.yaml │ ├── objectscript-quality.yml │ └── zpm.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── Dockerfile.iris ├── LICENSE ├── README.md ├── docker-compose.cache.yml ├── docker-compose.iris.yml ├── docker-compose.yml ├── entrypoint.sh ├── images ├── ENSLIB_5_1_0.png ├── IRISSYS_5_1_0.png ├── IRISSYS_5_1_1.png ├── MapView.png ├── TESTDB_20_1_0.png ├── TESTDB_20_1_1.png └── TreeView.png ├── makefile ├── module.xml ├── server ├── .dockerignore ├── Dockerfile ├── Dockerfile.iris └── src │ ├── Blocks │ ├── BlocksInstaller.cls │ ├── BlocksMap.cls │ ├── Router.cls │ ├── StandaloneInstaller.cls │ └── WebSocket.cls │ └── DevInstaller.cls └── web ├── .babelrc ├── .eslintrc.json ├── Dockerfile ├── index.html ├── js ├── blocksViewer.js ├── joint.shapes.blocks.js ├── main.js ├── mapViewer.js └── wsEventDispatcher.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── styles └── main.scss └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | **/node_modules 3 | build 4 | *.log 5 | *.md 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.cls] 13 | indent_size = 2 14 | indent_style = space 15 | insert_final_newline = false 16 | 17 | [*.md] 18 | insert_final_newline = false 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - release 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | variant: [ 10 | "cache", 11 | "iris" 12 | ] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@master 16 | - name: Login to Docker Registry 17 | run: docker login --username $DOCKER_USERNAME --password $DOCKER_PASSWORD 18 | env: 19 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 20 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 21 | - name: Build and push 22 | run: make ${{ matrix.variant }} 23 | -------------------------------------------------------------------------------- /.github/workflows/objectscript-quality.yml: -------------------------------------------------------------------------------- 1 | name: objectscriptquality 2 | on: push 3 | 4 | jobs: 5 | linux: 6 | name: Linux build 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Execute ObjectScript Quality Analysis 11 | run: wget https://raw.githubusercontent.com/litesolutions/objectscriptquality-jenkins-integration/master/iris-community-hook.sh && sh ./iris-community-hook.sh 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/zpm.yml: -------------------------------------------------------------------------------- 1 | name: zpm 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: '10.x' 14 | - run: cd web && npm ci 15 | - run: cd web && npm run build:prod 16 | - name: zpm 17 | uses: isc-zpm/actions@master 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | /temp/ 4 | build 5 | out 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "objectscript.conn": { 3 | "active": true, 4 | "ns": "BLOCKS", 5 | "docker-compose": { 6 | "service": "server" 7 | }, 8 | "links": { 9 | "BlocksExplorer": "http://localhost/" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG CACHE_VERSION=2018.1 2 | FROM node:8-alpine AS web 3 | 4 | WORKDIR /opt/blocks/ 5 | 6 | COPY web ./ 7 | 8 | RUN npm install \ 9 | && export PATH="$PATH:./node_modules/.bin" \ 10 | && webpack --mode production 11 | 12 | FROM daimor/intersystems-cache:${CACHE_VERSION} 13 | 14 | WORKDIR /opt/blocks 15 | 16 | RUN yum -y install ImageMagick 17 | 18 | COPY ./server/src/ ./src 19 | COPY --from=web /opt/blocks/build/ /usr/cachesys/csp/blocks/ 20 | 21 | RUN ccontrol start $ISC_PACKAGE_INSTANCENAME quietly \ 22 | && echo -e "" \ 23 | "do ##class(%SYSTEM.OBJ).Load(\"/opt/blocks/src/DevInstaller.cls\",\"cdk\")\n" \ 24 | "set sc=##class(Blocks.DevInstaller).setupWithVars(\"/opt/blocks/\")\n" \ 25 | "do:'sc \$zu(4,\$j,1)\n" \ 26 | "halt\n" \ 27 | | csession $ISC_PACKAGE_INSTANCENAME -UUSER \ 28 | # Stop Caché instance 29 | && ccontrol stop $ISC_PACKAGE_INSTANCENAME quietly 30 | 31 | VOLUME [ "/opt/blocks/db" ] 32 | 33 | COPY entrypoint.sh / 34 | 35 | ENTRYPOINT [ "/entrypoint.sh" ] 36 | -------------------------------------------------------------------------------- /Dockerfile.iris: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine AS web 2 | 3 | WORKDIR /opt/blocks/ 4 | 5 | COPY web ./ 6 | 7 | RUN npm install \ 8 | && export PATH="$PATH:./node_modules/.bin" \ 9 | && webpack --mode production 10 | 11 | FROM intersystemsdc/iris-community 12 | 13 | WORKDIR /opt/blocks 14 | 15 | USER root 16 | 17 | RUN chown ${ISC_PACKAGE_MGRUSER}:${ISC_PACKAGE_IRISGROUP} ${PWD} && \ 18 | apt-get -y update && \ 19 | DEBIAN_FRONTEND=noninteractive apt-get -y install imagemagick --no-install-recommends && \ 20 | rm -rf /var/lib/apt/lists/* 21 | 22 | USER irisowner 23 | 24 | COPY ./server/src/ ./src 25 | COPY --from=web /opt/blocks/build/ /usr/irissys/csp/blocks/ 26 | 27 | RUN iris start $ISC_PACKAGE_INSTANCENAME quietly \ 28 | && /bin/echo -e "do ##class(%SYSTEM.OBJ).Load(\"/opt/blocks/src/DevInstaller.cls\",\"cdk\")\n" \ 29 | "set sc=##class(Blocks.DevInstaller).setupWithVars(\"/opt/blocks/\")\n" \ 30 | "do:'sc \$zu(4,\$j,1)\n" \ 31 | "Halt\n" \ 32 | | iris session ${ISC_PACKAGE_INSTANCENAME} \ 33 | && iris stop $ISC_PACKAGE_INSTANCENAME quietly 34 | 35 | VOLUME [ "/opt/blocks/db" ] 36 | 37 | COPY entrypoint.sh / 38 | 39 | ENTRYPOINT [ "/entrypoint.sh" ] 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2019 Dmitry Maslennikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![zpm](https://github.com/daimor/BlocksExplorer/workflows/zpm/badge.svg) 2 | 3 | # Blocks Explorer 4 | [![Quality Gate Status](https://community.objectscriptquality.com/api/project_badges/measure?project=intersystems_iris_community%2FCacheBlocksExplorer&metric=alert_status)](https://community.objectscriptquality.com/dashboard?id=intersystems_iris_community%2FCacheBlocksExplorer) 5 | 6 | Database Blocks Explorer for InterSystems IRIS/Caché 7 | 8 | #### Key features 9 | ##### Tree explorer 10 | + Shows tree of database blocks; 11 | + Export tree as SVG or PNG image; 12 | + Shows every node in the block; 13 | + Open any block just by clicking on node in parent block; 14 | + Reload block info by clicking at the same node second time; 15 | + Zoom in and out, fit and navigator; 16 | + Easy way to switch between view modes (tree/map); 17 | 18 | ##### Fragmentation map 19 | + Shows every block with the same colour for every globals; 20 | + Legend for globals; 21 | 22 | #### Run with Docker 23 | You need license key for Caché or IRIS on RedHat systems. 24 | ##### Caché 25 | ``` 26 | docker run -d --name blocksexplorer --rm \ 27 | -p 57772:57772 \ 28 | -v /opt/some/database/for/test:/opt/blocks/db/test \ 29 | -v ~/cache.key:/usr/cachesys/mgr/cache.key \ 30 | daimor/blocksexplorer:cache 31 | ``` 32 | Generate blocks map as image file in `out` directory 33 | ``` 34 | docker run -it --rm \ 35 | -v /opt/some/database/for/test:/opt/blocks/db/test \ 36 | -v `pwd`/out:/opt/blocks/out \ 37 | daimor/blocksexplorer:cache generate 38 | ``` 39 | 40 | ##### IRIS 41 | ``` 42 | docker run -d --name blocksexplorer --rm \ 43 | -p 52773:52773 \ 44 | -v /opt/some/database/for/test:/opt/blocks/db/test \ 45 | daimor/blocksexplorer:iris 46 | ``` 47 | 48 | Generate blocks map as image file in `out` directory 49 | ``` 50 | docker run -it --rm \ 51 | -v /opt/some/database/for/test:/opt/blocks/db/test \ 52 | -v `pwd`/out:/opt/blocks/out \ 53 | daimor/blocksexplorer:iris generate 54 | ``` 55 | 56 | 57 | #### Development mode 58 | Run with docker-compose, will start web part with hot reloading. 59 | ``` 60 | docker-compose up -d --build 61 | ``` 62 | It will start server base on IRIS 63 | To start on Caché use this command 64 | ``` 65 | MODE=cache docker-compose up -d --build 66 | ``` 67 | By default running on 80 port. To start using it, just open http://localhost/ 68 | 69 | ## Screenshots 70 | 71 | ![Tree](https://raw.githubusercontent.com/daimor/BlocksExplorer/master/images/TreeView.png) 72 | ![Map](https://raw.githubusercontent.com/daimor/BlocksExplorer/master/images/MapView.png) 73 | 74 | ## CLI mode 75 | 76 | Using prebuild docker image gives a way to generate a picture for any IRIS or Caché database. 77 | Use docker image `daimor/blocksexplorer:iris` for IRIS or `daimor/blocksexplorer:cache` for Caché Databases. 78 | Those images accepts command `generate` with arguments 79 | 80 | * path to the tested databases inside a container, by default `/db`, can be omited 81 | * cellSize - size of the cell in pixels, where each cell represents particular database's block, by default 1 82 | * cellSpace - sorrounding space between cell, by default 0 83 | * showFill - sign to show how much block fill by data, by default 0 84 | 85 | This tool generates a square picture in folder /out inside a container in formats BMP and PNG. 86 | 87 | So, with command like this 88 | ``` 89 | docker run -v `pwd`/out:/out daimor/blocksexplorer:iris generate 20 1 1 90 | ``` 91 | It will generate this picture for an empty database. 92 | 93 | ![TESTDB](https://raw.githubusercontent.com/daimor/BlocksExplorer/master/images/TESTDB_20_1_1.png) 94 | With a lighter color visible that most of the blocks just empty. 95 | 96 | The same test empty database, but with showFill=0 97 | ``` 98 | docker run -v `pwd`/out:/out daimor/blocksexplorer:iris generate 20 1 0 99 | ``` 100 | Blocks have different colors but just for globals, and does not show how much it fill. 101 | 102 | ![TESTDB](https://raw.githubusercontent.com/daimor/BlocksExplorer/master/images/TESTDB_20_1_0.png) 103 | 104 | More examples 105 | ENSLIB 106 | ``` 107 | docker run -v `pwd`/out:/out daimor/blocksexplorer:iris generate /usr/irissys/mgr/enslib 5 1 108 | ``` 109 | ![ENSLIB](https://raw.githubusercontent.com/daimor/BlocksExplorer/master/images/ENSLIB_5_1_0.png) 110 | 111 | IRISSYS 112 | ``` 113 | docker run -v `pwd`/out:/out daimor/blocksexplorer:iris generate /usr/irissys/mgr/ 5 1 1 114 | ``` 115 | ![IRISSYS](https://raw.githubusercontent.com/daimor/BlocksExplorer/master/images/IRISSYS_5_1_1.png) 116 | 117 | 118 | For large databases, would not recommend to use have too big cellSize. 119 | 120 | ### Useful Links 121 | 122 | There you can find more about database internals, and how to use this tool. 123 | * [Internal Structure of Caché Database Blocks, Part 1](https://community.intersystems.com/post/internal-structure-cach%C3%A9-database-blocks-part-1) 124 | * [Internal Structure of Caché Database Blocks, Part 2](https://community.intersystems.com/post/internal-structure-cach%C3%A9-database-blocks-part-2) 125 | * [Internal Structure of Caché Database Blocks, Part 3](https://community.intersystems.com/post/internal-structure-cach%C3%A9-database-blocks-part-3) 126 | 127 | -------------------------------------------------------------------------------- /docker-compose.cache.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | server: 5 | build: 6 | context: server 7 | args: 8 | CACHE_VERSION: ${CACHE_VERSION-2018.1} 9 | ports: 10 | - 57772 11 | - 1972 12 | volumes: 13 | - ~/cache.key:/usr/cachesys/mgr/cache.key 14 | - ./server/src:/opt/blocks/src 15 | web: 16 | environment: 17 | - DB_PORT=57772 18 | -------------------------------------------------------------------------------- /docker-compose.iris.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | server: 5 | build: 6 | context: server 7 | dockerfile: Dockerfile.iris 8 | command: --check-caps false 9 | ports: 10 | - 1972 11 | - 52773 12 | volumes: 13 | # - ~/iris.key:/usr/irissys/mgr/iris.key 14 | - ./server/src:/opt/blocks/src 15 | web: 16 | environment: 17 | - DB_PORT=52773 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | services: 4 | server: 5 | extends: 6 | file: docker-compose.${MODE:-iris}.yml 7 | service: server 8 | volumes: 9 | - ./out:/opt/blocks/out 10 | web: 11 | extends: 12 | file: docker-compose.${MODE:-iris}.yml 13 | service: web 14 | build: 15 | context: web 16 | environment: 17 | - DB_HOST=server 18 | - WEB_PORT=${WEB_PORT-80} 19 | ports: 20 | - "${WEB_PORT-80}:${WEB_PORT-80}" 21 | volumes: 22 | - node_modules:/opt/app/node_modules 23 | - web_build:/opt/app/build 24 | - ./web:/opt/app 25 | volumes: 26 | node_modules: null 27 | web_build: null 28 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -lt 1 ]; then 4 | # just start server 5 | if [ -f /iris-main ]; then 6 | /iris-main 7 | else 8 | /ccontainermain 9 | fi 10 | exit 11 | fi 12 | 13 | COMMAND=$1 14 | if [ -x "$(command -v ccontrol)" ]; then 15 | CCONTROL=ccontrol 16 | else 17 | CCONTROL=iris 18 | fi 19 | 20 | if [ "${COMMAND,,}" = "generate" ]; then 21 | shift 22 | re='^[0-9]+$' 23 | if ! [[ "$1" =~ $re ]]; then 24 | DATABASE=$1 25 | shift 26 | fi 27 | OutputFolder=/opt/blocks/out/ 28 | CellSize=${1:-1} 29 | CellSpace=${2:-0} 30 | ShowFill=${3:-0} 31 | DATABASE=${DATABASE:-/db} 32 | 33 | echo 34 | echo "Starting server..." 35 | $CCONTROL start $ISC_PACKAGE_INSTANCENAME quietly 36 | echo 37 | echo "Generating image..." 38 | echo "Database = \"$DATABASE\"" 39 | echo "OutputFolder = \"$OutputFolder\"" 40 | echo "CellSize = $CellSize" 41 | echo "CellSpace = $CellSpace" 42 | echo "ShowFill = $ShowFill" 43 | rm ${OutputFolder}BlocksMap.{png,bmp} 44 | $CCONTROL session $ISC_PACKAGE_INSTANCENAME -UBLOCKS "##class(Blocks.BlocksMap).Generate(\"$DATABASE\",\"${OutputFolder}\",\"${CellSize}\",\"${CellSpace}\",\"${ShowFill}\")" 45 | echo 46 | echo "Stopping server..." 47 | $CCONTROL stop $ISC_PACKAGE_INSTANCENAME quietly 48 | echo "Finished" 49 | else 50 | /bin/echo -e "" \ 51 | "Available commands:\n\n" \ 52 | " help - this help\n" \ 53 | " generate - will generate BlocksMap for the database located in /opt/blocks/db/test/\n" \ 54 | " as an image in bmp and png format in folder /opt/blocks/out\n" 55 | " \n" 56 | fi 57 | -------------------------------------------------------------------------------- /images/ENSLIB_5_1_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/ENSLIB_5_1_0.png -------------------------------------------------------------------------------- /images/IRISSYS_5_1_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/IRISSYS_5_1_0.png -------------------------------------------------------------------------------- /images/IRISSYS_5_1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/IRISSYS_5_1_1.png -------------------------------------------------------------------------------- /images/MapView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/MapView.png -------------------------------------------------------------------------------- /images/TESTDB_20_1_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/TESTDB_20_1_0.png -------------------------------------------------------------------------------- /images/TESTDB_20_1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/TESTDB_20_1_1.png -------------------------------------------------------------------------------- /images/TreeView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daimor/BlocksExplorer/5a213a8b15e2194213c07f4aecefbca6d2c5c83f/images/TreeView.png -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | IMAGE=daimor/blocksexplorer 2 | 3 | .PHONY: clean cache iris web 4 | 5 | build: clean web 6 | 7 | web: 8 | cd web && npm ci && npm run build:prod 9 | 10 | clean: 11 | rm -rf web/build 12 | 13 | cache: 14 | docker build -t $(IMAGE):cache . 15 | docker push $(IMAGE):cache 16 | 17 | iris: 18 | docker build -f Dockerfile.iris -t $(IMAGE):iris . 19 | docker push $(IMAGE):iris 20 | -------------------------------------------------------------------------------- /module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BlocksExplorer 5 | 2.2.1 6 | module 7 | server/src 8 | 9 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | .* 3 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG CACHE_VERSION=2017.2 2 | FROM daimor/intersystems-cache:${CACHE_VERSION} 3 | 4 | WORKDIR /opt/blocks 5 | 6 | COPY ./src/ ./src 7 | 8 | RUN ccontrol start $ISC_PACKAGE_INSTANCENAME quietly \ 9 | && echo -e "" \ 10 | "do ##class(%SYSTEM.OBJ).Load(\"/opt/blocks/src/DevInstaller.cls\",\"cdk\")\n" \ 11 | "set sc=##class(Blocks.DevInstaller).setupWithVars(\"/opt/blocks/\")\n" \ 12 | "do:'sc \$zu(4,\$j,1)\n" \ 13 | "halt\n" \ 14 | | csession $ISC_PACKAGE_INSTANCENAME -UUSER \ 15 | # Stop Caché instance 16 | && ccontrol stop $ISC_PACKAGE_INSTANCENAME quietly 17 | 18 | VOLUME [ "/opt/blocks/db" ] 19 | -------------------------------------------------------------------------------- /server/Dockerfile.iris: -------------------------------------------------------------------------------- 1 | # ARG IRIS_VERSION=2020.1.0.202.0 2 | # FROM store/intersystems/iris-community:${IRIS_VERSION} 3 | FROM intersystemsdc/iris-community 4 | 5 | WORKDIR /opt/blocks 6 | 7 | USER root 8 | 9 | RUN chown ${ISC_PACKAGE_MGRUSER}:${ISC_PACKAGE_IRISGROUP} ${PWD} && \ 10 | apt-get -y update && \ 11 | DEBIAN_FRONTEND=noninteractive apt-get -y install imagemagick --no-install-recommends && \ 12 | rm -rf /var/lib/apt/lists/* 13 | 14 | USER irisowner 15 | 16 | COPY ./src/ ./src 17 | 18 | RUN iris start $ISC_PACKAGE_INSTANCENAME quietly && \ 19 | /bin/echo -e "" \ 20 | "do ##class(%SYSTEM.OBJ).Load(\"/opt/blocks/src/DevInstaller.cls\",\"cdk\")\n" \ 21 | "set sc=##class(Blocks.DevInstaller).setupWithVars(\"/opt/blocks/\")\n" \ 22 | "do:'sc \$zu(4,\$j,1)\n" \ 23 | "do INT^JRNSTOP kill ^%SYS(\"Journal\")\n" \ 24 | "do ##class(Security.Users).UnExpireUserPasswords(\"*\")\n" \ 25 | "halt" \ 26 | | iris session $ISC_PACKAGE_INSTANCENAME -U%SYS && \ 27 | iris stop $ISC_PACKAGE_INSTANCENAME quietly && \ 28 | rm $ISC_PACKAGE_INSTALLDIR/mgr/IRIS.WIJ && \ 29 | rm $ISC_PACKAGE_INSTALLDIR/mgr/journal/* 30 | 31 | VOLUME [ "/opt/blocks/db" ] 32 | 33 | -------------------------------------------------------------------------------- /server/src/Blocks/BlocksInstaller.cls: -------------------------------------------------------------------------------- 1 | Class Blocks.BlocksInstaller Extends %Projection.AbstractProjection 2 | { 3 | 4 | Projection Reference As BlocksInstaller; 5 | 6 | Parameter CSPAPP As %String = "/blocks"; 7 | 8 | Parameter CSPAPPDESCRIPTION As %String = "A WEB application for Cache Blocks Explorer."; 9 | 10 | Parameter ROUTER As %String = "Blocks.Router"; 11 | 12 | /// This method is invoked when a class is compiled. 13 | ClassMethod CreateProjection(cls As %String, ByRef params) As %Status 14 | { 15 | set ns=$namespace 16 | new $namespace 17 | znspace "%SYS" 18 | 19 | if ('##class(Security.Applications).Exists(..#CSPAPP)) { 20 | do ##class(Security.System).GetInstallationSecuritySetting(.security) 21 | set cspProperties("AutheEnabled") = $select((security="None"):64,1:32) 22 | set cspProperties("NameSpace") = ns 23 | set cspProperties("Description") = ..#CSPAPPDESCRIPTION 24 | set cspProperties("DispatchClass") = ..#ROUTER 25 | write !, "Creating WEB application """_..#CSPAPP_"""..." 26 | $$$ThrowOnError(##class(Security.Applications).Create(..#CSPAPP, .cspProperties)) 27 | write !, "WEB application """_..#CSPAPP_""" created." 28 | if ##class(%Studio.General).GetWebServerPort(,,,.url) { 29 | write !, "You can now open it with a link: "_url_$p(..#CSPAPP,"/",2,*)_"/" 30 | } 31 | } else { 32 | write !, "WEB application """_..#CSPAPP_""" already exists, so it is ready to use." 33 | } 34 | Quit $$$OK 35 | } 36 | 37 | /// This method is invoked when a class is 'uncompiled'. 38 | ClassMethod RemoveProjection(cls As %String, ByRef params, recompile As %Boolean) As %Status 39 | { 40 | new $namespace 41 | znspace "%SYS" 42 | 43 | if (##class(Security.Applications).Exists(..#CSPAPP)) { 44 | w !, "Deleting WEB application """_..#CSPAPP_"""..." 45 | do ##class(Security.Applications).Delete(..#CSPAPP) 46 | w !, "WEB application """_..#CSPAPP_""" was successfully removed." 47 | } 48 | QUIT $$$OK 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /server/src/Blocks/BlocksMap.cls: -------------------------------------------------------------------------------- 1 | Class Blocks.BlocksMap 2 | { 3 | 4 | Parameter DefaultOutputFolder = "/opt/blocks/out/"; 5 | 6 | ClassMethod Generate(aDirectory = "", pOutputFolder = {..#DefaultOutputFolder}, cellSize = 1, cellSpace = 0, showFill = 0) As %Status 7 | { 8 | If '##class(%File).DirectoryExists(pOutputFolder) { 9 | Quit $$$ERROR($$$GeneralError, "OutputFolder must exist") 10 | } 11 | 12 | Set tSC = ..GenerateMap(aDirectory) 13 | If $$$ISERR(tSC) { 14 | Do $System.OBJ.DisplayError(tSC) 15 | Quit tSC 16 | } 17 | 18 | Set tSC = ..DrawMap(.pOutputFolder, cellSize, cellSpace, showFill) 19 | If $$$ISERR(tSC) { 20 | Do $System.OBJ.DisplayError(tSC) 21 | Quit tSC 22 | } 23 | 24 | Set tSC = ..ConvertMap(.pOutputFolder) 25 | If $$$ISERR(tSC) { 26 | Do $System.OBJ.DisplayError(tSC) 27 | Quit tSC 28 | } 29 | Quit $$$OK 30 | } 31 | 32 | ClassMethod ConvertMap(pOutputFolder = {..#DefaultOutputFolder}) As %Status 33 | { 34 | Set fileNameBMP = ##class(%File).NormalizeFilename("BlocksMap.bmp", pOutputFolder) 35 | Set fileNamePNG = ##class(%File).NormalizeFilename("BlocksMap.png", pOutputFolder) 36 | Do ##class(%File).Delete(fileNamePNG) 37 | 38 | Set ok = $ZF(-100, "/SHELL", "convert", fileNameBMP, fileNamePNG) 39 | Quit $$$OK 40 | } 41 | 42 | ClassMethod DrawMap(pOutputFolder = {..#DefaultOutputFolder}, cellSize = 1, cellSpace = 0, showFill = 0) As %Status 43 | { 44 | Set mapGN = $Name(^||BlocksMap) 45 | Do ..initColors() 46 | Set cellSpace = $System.SQL.CEILING(cellSpace / 2) * 2 47 | 48 | Set fileName = ##class(%File).NormalizeFilename("BlocksMap.bmp", pOutputFolder) 49 | Do ##class(%File).Delete(fileName) 50 | 51 | Set file = ##class(%Stream.FileBinary).%New() 52 | Set file.Filename = fileName 53 | Do file.Clear() 54 | 55 | Set $Listbuild(blocks, size) = $Get(@mapGN) 56 | 57 | Set width = size * ( cellSize + cellSpace ) 58 | Set height = width 59 | 60 | Set bytesPerPixel = 3 61 | Set bfOffBits = 54 62 | 63 | Set bfSize = bfOffBits + (bytesPerPixel*width*height) 64 | Set fileHeader = "" 65 | _ "BM" // WORD bfType 66 | _ ..justify(bfSize, 4) // DWORD bfSize 67 | _ ..justify(0, 2) // WORD bfReserved1 68 | _ ..justify(0, 2) // WORD bfReserved2 69 | _ ..justify(bfOffBits, 4) // DWORD bfOffBits 70 | 71 | Set biSize = 40 72 | Set biWidth = width 73 | Set biHeight = height 74 | Set biPlanes = 1 75 | Set biBitCount = bytesPerPixel * 8 76 | Set biCompression = 0 77 | Set biSizeImage = 0 78 | Set biXPelsPerMeter = 0 79 | Set biYPelsPerMeter = 0 80 | Set biClrUsed = 0 81 | Set biClrImportant = 0 82 | 83 | Set infoHeader = "" 84 | _ ..justify(biSize, 4) // DWORD biSize; 85 | _ ..justify(biWidth, 4) // LONG biWidth; 86 | _ ..justify(biHeight, 4) // LONG biHeight; 87 | _ ..justify(biPlanes, 2) // WORD biPlanes; 88 | _ ..justify(biBitCount, 2) // WORD biBitCount; 89 | _ ..justify(biCompression, 4) // DWORD biCompression; 90 | _ ..justify(biSizeImage, 4) // DWORD biSizeImage; 91 | _ ..justify(biXPelsPerMeter, 4) // LONG biXPelsPerMeter; 92 | _ ..justify(biYPelsPerMeter, 4) // LONG biYPelsPerMeter; 93 | _ ..justify(biClrUsed, 4) // DWORD biClrUsed; 94 | _ ..justify(biClrImportant, 4) // DWORD biClrImportant; 95 | 96 | 97 | Do file.Write(fileHeader) 98 | Do file.Write(infoHeader) 99 | 100 | Set paddingSize = (4 - (width * bytesPerPixel) # 4) # 4 101 | For y=biHeight:-1:1 { 102 | Set line = "" 103 | For x=1:1:biWidth { 104 | Set $Listbuild(red, green, blue) = ..getColor(y, x, size, cellSize, cellSpace, showFill) 105 | Set color = "" 106 | _ ..justify(red, 1) 107 | _ ..justify(green, 1) 108 | _ ..justify(blue, 1) 109 | Set line = line _ color 110 | } 111 | Do file.Write(line _ ..justify(0, paddingSize)) 112 | } 113 | 114 | Set tSC = file.%Save() 115 | Do file.%Close() 116 | Quit tSC 117 | } 118 | 119 | ClassMethod initColors() As %Status 120 | { 121 | Set mapGN = $Name(^||BlocksMap) 122 | Set maxCount = $Get(@mapGN@("Globals")) 123 | Kill @mapGN@("Colors") 124 | 125 | For i=1:1:maxCount { 126 | Set ksi = i / maxCount 127 | 128 | If (ksi < 0.5) { 129 | Set red = ksi * 2 130 | Set blue = (0.5 - ksi) * 2 131 | } 132 | Else { 133 | Set red = (1.0 - ksi) * 2 134 | Set blue = (ksi - 0.5) * 2 135 | } 136 | 137 | If (ksi >= 0.3) && (ksi < 0.8) { 138 | Set green = (ksi - 0.3) * 2 139 | } 140 | ElseIf (ksi < 0.3) { 141 | Set green = (0.3 - ksi) * 2 142 | } 143 | Else { 144 | Set green = (1.3 - ksi) * 2 145 | } 146 | 147 | Set red = $System.SQL.FLOOR(red * 255) 148 | Set green = $System.SQL.FLOOR(green * 255) 149 | Set blue = $System.SQL.FLOOR(blue * 255) 150 | 151 | Set color = $Listbuild(red, green, blue) 152 | Set @mapGN@("Colors", i) = color 153 | } 154 | Quit $$$OK 155 | } 156 | 157 | ClassMethod getColor(y, x, size, cellSize, cellSpace, showFill) As %List 158 | { 159 | Set mapGN = $Name(^||BlocksMap) 160 | 161 | Set padding = 0 162 | Set spacing = 0 163 | If cellSpace { 164 | Set padding = cellSpace \ 2 165 | 166 | Set spacing = (y <= padding) || (x <= padding) 167 | Set spacing = spacing || (x # (cellSize + cellSpace) <= padding) 168 | Set spacing = spacing || (y # (cellSize + cellSpace) <= padding) 169 | } 170 | Set blockX = x - 1 \ (cellSize + cellSpace) + 1 171 | Set blockY = y - 1 \ (cellSize + cellSpace) + 1 172 | 173 | If spacing { 174 | Set red = 222 175 | Set green = 222 176 | Set blue = 222 177 | } 178 | ElseIf $Data(@mapGN@("Map", blockY, blockX), blockInfo) { 179 | Set $Listbuild(globalNum, fill) = blockInfo 180 | Set $Listbuild(red, green, blue) = $Get(@mapGN@("Colors", globalNum)) 181 | if (showFill) { 182 | Set fillSize = $System.SQL.CEILING(fill / 100 * cellSize) + 1 183 | Set:fillSize<1 fillSize = 1 184 | if ( y # (cellSize + cellSpace) > fillSize) { 185 | Set alpha = 0.5 186 | Set red = (1-alpha) * 255 + (alpha * red) 187 | Set green = (1-alpha) * 255 + (alpha * green) 188 | Set blue = (1-alpha) * 255 + (alpha * blue) 189 | } 190 | } 191 | } 192 | Else { 193 | Set red = 255 194 | Set green = 255 195 | Set blue = 255 196 | } 197 | 198 | Quit $Listbuild(red, green, blue) 199 | } 200 | 201 | ClassMethod GenerateMap(aDirectory = "") As %Status 202 | { 203 | #Define SYS do ##Continue 204 | . New $Namespace Set $Namespace = "%SYS" 205 | 206 | If aDirectory="" { 207 | Quit $$$ERROR($$$GeneralError, "Directory must be specified") 208 | } 209 | $$$SYS Set db = ##class(SYS.Database).%OpenId(aDirectory) 210 | If '$IsObject(db) { 211 | Quit $$$ERROR($$$GeneralError, "Specified database does not exists or does not available") 212 | } 213 | 214 | Set mapGN = $Name(^||BlocksMap) 215 | Kill @mapGN 216 | 217 | Set blocks = db.Blocks 218 | Set size = $System.SQL.CEILING($ZSqr(blocks)) 219 | Set @mapGN = $Listbuild(blocks, size) 220 | 221 | Set resName = "blocks"_$Job 222 | If '$System.Event.Defined(resName) { 223 | Do $System.Event.Create(resName) 224 | } 225 | 226 | Job ##class(Blocks.WebSocket).GetBlocks(aDirectory, 0, resName) 227 | 228 | Set $Listbuild(sc, data)=$System.Event.WaitMsg(resName, 0) 229 | Set child = $ZChild 230 | 231 | Set atEnd = 0 232 | While 'atEnd { 233 | Set $Listbuild(sc,data)=$System.Event.WaitMsg(resName, 0) 234 | If sc<=0,'$Data(^$JOB(child)) { 235 | Set atEnd = 1 236 | Quit 237 | } 238 | Continue:data="" 239 | Set $Listbuild(parent, block, global, fill) = data 240 | Continue:global="" 241 | If '$Data(@mapGN@("Globals", global), globalNum) { 242 | Set globalNum = $Increment(@mapGN@("Globals")) 243 | Set @mapGN@("Globals", global) = globalNum 244 | } 245 | 246 | Set x = block - 1 # size + 1 247 | Set y = block - 1 \ size + 1 248 | Set @mapGN@("Map", y, x) = $Listbuild(globalNum, fill) 249 | } 250 | 251 | Quit $$$OK 252 | } 253 | 254 | ClassMethod justify(val, len = 4, back = 1) As %String 255 | { 256 | Set result = "" 257 | For i=1:1:len { 258 | Set:'back result = $Char(val#256) _ result 259 | Set:back result = result _ $Char(val#256) 260 | Set val = val\256 261 | } 262 | Quit result 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /server/src/Blocks/Router.cls: -------------------------------------------------------------------------------- 1 | Include %syDatabase 2 | 3 | Class Blocks.Router Extends %CSP.REST 4 | { 5 | 6 | XData UrlMap 7 | { 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | } 17 | 18 | ClassMethod WebSocket() As %Status 19 | { 20 | Do ##class(%Library.Device).ReDirectIO(0) 21 | Quit ##class(Blocks.WebSocket).Page(0) 22 | } 23 | 24 | ClassMethod outputJSON(data) 25 | { 26 | #dim %response As %CSP.Response 27 | set %response.ContentType = "application/json" 28 | 29 | if $data(data),$isobject(data) { 30 | if data.%IsA("%ZEN.proxyObject") { 31 | quit data.%ToJSON() 32 | } else { 33 | write data.%ToJSON() 34 | quit $$$OK 35 | } 36 | } 37 | 38 | quit $$$OK 39 | } 40 | 41 | ClassMethod GetStatic(pName As %String = "") As %Status 42 | { 43 | #dim %response As %CSP.Response 44 | 45 | set tName = pName 46 | if tName = "" set tName = "index.html" 47 | set tName = $tr(tName,".","_") 48 | set ext = $zcvt($p(tName,"_",*),"l") 49 | if ##class(%CSP.StreamServer).FileClassify(ext,.type) { 50 | set %response.ContentType = type 51 | } 52 | set xdata = ##class(%Dictionary.XDataDefinition).%OpenId(..%ClassName(1)_"||"_tName) 53 | if $isobject(xdata) { 54 | set status=##class(%XML.TextReader).ParseStream(xdata.Data, .textreader) 55 | set isBase64=xdata.Description["*base64*" 56 | if $isobject(textreader) { 57 | set data="" 58 | while textreader.Read() { 59 | if (textreader.NodeType="chars") { 60 | if isBase64 { 61 | set data=data_textreader.Value 62 | set data=$translate(data,$char(13,10)) 63 | set data4Decode=$extract(data,1,$length(data)\4*4) 64 | write $system.Encryption.Base64Decode(data4Decode) 65 | set data=$extract(data,$length(data4Decode)+1,*) 66 | } else { 67 | write textreader.Value 68 | } 69 | } 70 | } 71 | write $system.Encryption.Base64Decode(data) 72 | quit $$$OK 73 | } else { 74 | quit xdata.Data.OutputToDevice() 75 | } 76 | } 77 | 78 | set tName = %request.GetCgiEnv("SCRIPT_FILENAME") 79 | if pName = "" set tName = ##class(%File).NormalizeFilename("index.html", tName) 80 | if ##class(%File).Exists(tName) { 81 | set stream = ##class(%Stream.FileBinary).%New() 82 | set stream.Filename = tName 83 | quit stream.OutputToDevice() 84 | } 85 | quit $$$OK 86 | } 87 | 88 | ClassMethod Ping() As %Status 89 | { 90 | set result = {} 91 | set result.ping = "pong" 92 | set result.session = %session.SessionId 93 | quit ..outputJSON(result) 94 | } 95 | 96 | ClassMethod DBList() As %Status 97 | { 98 | new $namespace 99 | znspace "%SYS" 100 | 101 | write "[" 102 | set first = 1 103 | 104 | set tStatement=##class(%SQL.Statement).%New() 105 | set tSC=tStatement.%PrepareClassQuery("Config.Databases","List") 106 | #dim rset As %SQL.StatementResult = tStatement.%Execute() 107 | while rset.%Next() { 108 | set dir=rset.%Get("Directory") 109 | set name=rset.%Get("Name") 110 | set was($zconvert(dir,"L"))=name 111 | do add(name,dir) 112 | } 113 | 114 | set tSC=tStatement.%PrepareClassQuery("SYS.Database","List") 115 | set rset = tStatement.%Execute() 116 | while rset.%Next() { 117 | set dir=rset.%Get("Directory") 118 | continue:$data(was($zconvert(dir,"L"))) 119 | do add(dir,dir) 120 | } 121 | write "]" 122 | quit ..outputJSON() 123 | add(name,dir) 124 | set db=##class(SYS.Database).%OpenId(dir) 125 | set blocks=db.Size*1024*1024/db.BlockSize 126 | Set tJSON = ##class(%ZEN.proxyObject).%New() 127 | set tJSON.name = name 128 | set tJSON.directory = dir 129 | set tJSON.blocks = blocks 130 | if 'first write ", " 131 | set first = 0 132 | do tJSON.%ToJSON() 133 | } 134 | 135 | ClassMethod BlockInfo(pBlock) [ ProcedureBlock = 0 ] 136 | { 137 | new $namespace 138 | znspace "%SYS" 139 | 140 | set Directory=$select($isobject($get(%request)):%request.Get("directory"),1:"") 141 | set:Directory="" Directory=$zutil(12,"samples") 142 | 143 | 144 | Set tJSON = ##class(%ZEN.proxyObject).%New() 145 | set tJSON.blockId = pBlock 146 | Set tJSON.nodes = ##class(%ListOfDataTypes).%New() 147 | 148 | #dim error As %Exception.AbstractException = "" 149 | try { 150 | set rc=$$FindPointerBlock^DMREPAIR(Directory,pBlock,.upblock) 151 | 152 | set rc=$$ParseRepairBlock^DMREPAIR(Directory,pBlock,,.OFF,.REPAIR,.REPPRINT,.REPVAL,.REPCCC,.REPLEN,.REPPAD,.REPSUB,.REPBIG,.REPINFO,.N,.TYPE,.LINK,.BIGCOUNT,.PNTLEN,.NEXTPNTLEN,.NEXTPNTVAL,.NEXTPNTOFF,.PNTREF,.NEXTPNTREF,.BLINCVER,.COLLATE,.GARTREE) 153 | 154 | set:$data(OFF) tJSON.offset=OFF 155 | if $data(TYPE) { 156 | set tJSON.type=TYPE 157 | set tJSON.typename=$$GetTypeName^DMREPAIR(TYPE) 158 | } 159 | set:$data(LINK) tJSON.link=LINK 160 | set:$data(BIGCOUNT) tJSON.bigCount=BIGCOUNT 161 | 162 | set:$data(PNTLEN) tJSON.pointerLength=PNTLEN 163 | set:$data(PNTREF("internal"),val) tJSON.pointerRefInt=$system.Encryption.Base64Encode(val) 164 | set:$data(PNTREF("printable"),val) tJSON.pointerRef=val 165 | 166 | set:$data(NEXTPNTLEN) tJSON.nextPointerLength=NEXTPNTLEN 167 | set:$data(NEXTPNTOFF) tJSON.nextPointerOffset=NEXTPNTOFF 168 | set:$data(NEXTPNTREF("internal"),val) tJSON.nextPointerRefInt=$system.Encryption.Base64Encode(val) 169 | set:$data(NEXTPNTREF("printable"),val) tJSON.nextPointerRef=val 170 | 171 | set:$data(COLLATE) tJSON.collate=COLLATE 172 | 173 | for i=1:1:N { 174 | 175 | set tNode=##class(%ArrayOfDataTypes).%New() 176 | do tNode.SetAt(i,"nodeId") 177 | 178 | do:$data(REPAIR(i),repint) tNode.SetAt($system.Encryption.Base64Encode(repint),"internal") 179 | for var="print","ccc","len","pad","sub","big","info" { 180 | set val=@("REP"_$zconvert(var,"U"))@(i) 181 | do tNode.SetAt(val,var) 182 | } 183 | if TYPE'=8,$data(REPVAL) { 184 | set val=$listfromstring(@("REPVAL")@(i)) 185 | if TYPE=9 { 186 | do tNode.SetAt($listget(val,1),"blockId") 187 | do tNode.SetAt($listget(val,3),"collate") 188 | } else { 189 | do tNode.SetAt($listget(val,2),"blockId") 190 | } 191 | } 192 | 193 | do tJSON.nodes.Insert(tNode) 194 | } 195 | } catch error { 196 | } 197 | 198 | if $isobject(error) { 199 | quit error.AsStatus() 200 | } 201 | 202 | quit ..outputJSON(tJSON) 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /server/src/Blocks/StandaloneInstaller.cls: -------------------------------------------------------------------------------- 1 | Class Blocks.StandaloneInstaller Extends %Projection.AbstractProjection 2 | { 3 | 4 | Projection Reference As Blocks.StandaloneInstaller; 5 | 6 | Parameter CSPAPP As %String = "/blocks"; 7 | 8 | Parameter CSPAPPDIR As %String = {##class(%File).SubDirectoryName($system.Util.InstallDirectory(), "csp"_$zcvt(..#CSPAPP,"l"), 1)}; 9 | 10 | Parameter CSPAPPDESC As %String = "A WEB application for Cache Blocks Explorer."; 11 | 12 | Parameter ROUTER As %String = "Blocks.Router"; 13 | 14 | Parameter NAMESPACE As %String = "BLOCKS"; 15 | 16 | Parameter DBNAME As %String = "BLOCKS"; 17 | 18 | Parameter DBPATH As %String = {$zu(12,$zcvt(..#DBNAME,"l"))}; 19 | 20 | Parameter AUTOINSTALL As %Boolean = 0; 21 | 22 | /// This method is invoked when a class is compiled. 23 | ClassMethod CreateProjection(classname As %String, ByRef parameters As %String, modified As %String, qstruct) As %Status 24 | { 25 | quit:'..#AUTOINSTALL $$$OK 26 | set xdata=##class(%Dictionary.XDataDefinition).%OpenId(..%ClassName(1)_"||Data",0) 27 | quit:'$isobject(xdata) $$$OK 28 | 29 | set logFile=##class(%File).TempFilename("setupBlocksExplorer") 30 | 31 | job ..setup():(:::logFile):0 32 | if '$test { 33 | quit $$$OK 34 | } 35 | set child=$zchild 36 | do { hang 0.1 } while $data(^$JOB(child)) 37 | 38 | set fs=##class(%Stream.FileCharacter).%New() 39 | set fs.Filename=logFile 40 | while 'fs.AtEnd { 41 | write !,fs.ReadLine() 42 | } 43 | if ##class(%File).Delete(logFile) 44 | quit $$$OK 45 | } 46 | 47 | ClassMethod setup(ByRef pVars, pLogLevel As %Integer = 3, pInstaller As %Installer.Installer, pLogger As %Installer.AbstractLogger) As %Status [ CodeMode = objectgenerator, Internal ] 48 | { 49 | do %code.WriteLine($c(9)_"if '$data(pVars(""FORCE"")) set pVars(""FORCE"")=$g(%qstruct(""force""))") 50 | do %code.WriteLine($c(9)_"set pVars(""CURRENTCLASS"")="""_%classname_"""") 51 | do %code.WriteLine($c(9)_"set pVars(""CURRENTNS"")=$namespace") 52 | do %code.WriteLine($c(9)_"if '$data(pVars(""NAMESPACE"")) set pVars(""NAMESPACE"")=..#NAMESPACE") 53 | do %code.WriteLine($c(9)_"if '$data(pVars(""DBNAME"")) set pVars(""DBNAME"")=..#DBNAME") 54 | do %code.WriteLine($c(9)_"if '$data(pVars(""DBPATH"")) set pVars(""DBPATH"")=..#DBPATH") 55 | do %code.WriteLine($c(9)_"if '$data(pVars(""CSPAPP"")) set pVars(""CSPAPP"")=..#CSPAPP") 56 | do %code.WriteLine($c(9)_"if '$data(pVars(""CSPAPPDIR"")) set pVars(""CSPAPPDIR"")=..#CSPAPPDIR") 57 | do %code.WriteLine($c(9)_"if '$data(pVars(""CSPAPPDESC"")) set pVars(""CSPAPPDESC"")=..#CSPAPPDESC") 58 | do %code.WriteLine($c(9)_"if '$data(pVars(""CSPAPPROUTER"")) set pVars(""CSPAPPROUTER"")=..#ROUTER") 59 | quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "setup") 60 | } 61 | 62 | XData setup [ XMLNamespace = INSTALLER ] 63 | { 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | } 82 | 83 | ClassMethod PrepareData() As %String 84 | { 85 | set tmpFile=##class(%File).TempFilename("xml") 86 | set tmpFileFS=##class(%Stream.FileBinary).%New() 87 | set tmpFileFS.Filename=tmpFile 88 | set xdata=##class(%Dictionary.XDataDefinition).%OpenId(..%ClassName(1)_"||Data",0) 89 | set status=##class(%XML.TextReader).ParseStream(xdata.Data, .textreader) 90 | set data="" 91 | while textreader.Read() { 92 | if (textreader.NodeType="chars") { 93 | set data=data_textreader.Value 94 | set data=$translate(data,$char(13,10)) 95 | set data4Decode=$extract(data,1,$length(data)\4*4) 96 | do tmpFileFS.Write($system.Encryption.Base64Decode(data4Decode)) 97 | set data=$extract(data,$length(data4Decode)+1,*) 98 | } 99 | } 100 | do tmpFileFS.Write($system.Encryption.Base64Decode(data)) 101 | do tmpFileFS.%Save() 102 | quit tmpFile 103 | } 104 | 105 | ClassMethod SetDispatchClass(pCSPName As %String = "", pDispatchClass As %String = "") As %Status 106 | { 107 | new $namespace 108 | znspace "%SYS" 109 | set props("DispatchClass")=pDispatchClass 110 | set props("Recurse")=1 111 | d ##class(Security.Applications).Modify(pCSPName,.props) 112 | quit $$$OK 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /server/src/Blocks/WebSocket.cls: -------------------------------------------------------------------------------- 1 | Class Blocks.WebSocket Extends %CSP.WebSocket 2 | { 3 | 4 | Parameter UseSession = 0; 5 | 6 | Property ResourceName As %String; 7 | 8 | Property ChildPID As %Integer; 9 | 10 | Property Event As %String; 11 | 12 | Method ResourceNameGet() As %String 13 | { 14 | set resName = "blocks" _ $job 15 | if '$system.Event.Defined(resName) { 16 | do $system.Event.Create(resName) 17 | } 18 | quit resName 19 | } 20 | 21 | Method OnPreServer() As %Status 22 | { 23 | Quit $$$OK 24 | } 25 | 26 | Method Server() As %Status 27 | { 28 | Set timeout=.1 29 | set quit=0 30 | #dim exception As %Exception.AbstractException 31 | Set len=32656 32 | while 'quit { 33 | try { 34 | Set data=..Read(.len, .status, timeout) 35 | If $$$ISERR(status),$$$GETERRORCODE(status) = $$$CSPWebSocketClosed { 36 | set quit=1 37 | quit 38 | } 39 | set timeouted = 0 40 | If $$$ISERR(status),$$$GETERRORCODE(status) = $$$CSPWebSocketTimeout { 41 | set timeouted = 1 42 | } 43 | if timeouted { 44 | set sc = ..SendBlocksData() 45 | } else { 46 | set sc = ..Action(data) 47 | } 48 | $$$ThrowOnError(sc) 49 | } catch exception { 50 | do ..Write("error", exception.DisplayString()) 51 | set quit=1 52 | } 53 | } 54 | Set status=..EndServer() 55 | Quit $$$OK 56 | } 57 | 58 | Method SendBlocksData() As %Status 59 | { 60 | set st = $$$OK 61 | 62 | quit:..Event="" $$$OK 63 | 64 | set child = ..ChildPID 65 | set resName = ..ResourceName 66 | 67 | set responseData = ##class(%DynamicArray).%New() 68 | kill blocksData 69 | set countData=0 70 | set atEnd=0 71 | while 'atEnd { 72 | set $lb(sc,data)=$system.Event.WaitMsg(resName, 0) 73 | if sc<=0,'$data(^$JOB(child)) { 74 | set atEnd=1 75 | quit 76 | } 77 | continue:data="" 78 | set $listbuild(parent,block,global,fill)=data 79 | if $p(..Event,"_",2)="tree" { 80 | if '$data(blocksData(parent)) { 81 | set blocksData(parent)=##class(%DynamicObject).%New() 82 | set blocksData(parent).block = parent 83 | set blocksData(parent).child = ##class(%DynamicArray).%New() 84 | do responseData.%Push(blocksData(parent)) 85 | } 86 | do blocksData(parent).child.%Push(block) 87 | } else { 88 | if '$data(blocksData(global)) { 89 | set blocksData(global)=##class(%DynamicObject).%New() 90 | set blocksData(global).global = global 91 | set blocksData(global).blocks = ##class(%DynamicArray).%New() 92 | do responseData.%Push(blocksData(global)) 93 | } 94 | set blockInfo = ##class(%DynamicArray).%New() 95 | do blockInfo.%Push(block) 96 | do blockInfo.%Push(fill) 97 | do blocksData(global).blocks.%Push(blockInfo) 98 | kill blockInfo 99 | } 100 | quit:$i(countData)>=1000 101 | } 102 | if responseData.Size > 0 { 103 | do ..Write(..Event, responseData) 104 | } 105 | if atEnd { 106 | set ..Event = "" 107 | } 108 | 109 | quit $$$OK 110 | } 111 | 112 | Method Action(obj As %DynamicObject) As %Status 113 | { 114 | set st = $$$OK 115 | #dim exception As %Exception.AbstractException 116 | try { 117 | quit:obj="" 118 | set action = obj.event 119 | set data = ##class(%DynamicArray).%New() 120 | if action="ping" { 121 | set sc=..Write(action, "pong") 122 | } elseif $p(action,"_")="blocks" { 123 | set asTree=($piece(action,"_",2)="tree") 124 | 125 | do $system.Event.Clear(..ResourceName) 126 | 127 | job ##class(Blocks.WebSocket).GetBlocks(obj.data.directory, asTree, ..ResourceName) 128 | set child=$zchild 129 | set ..ChildPID = child 130 | 131 | set ..Event = action 132 | } 133 | 134 | } catch exception { 135 | set st = exception.AsStatus() 136 | } 137 | quit st 138 | } 139 | 140 | Method OnPostServer() As %Status 141 | { 142 | Quit $$$OK 143 | } 144 | 145 | ClassMethod GetBlocks(aDirectory As %String = "", aAsTree As %Boolean = 0, ResourceName As %String = "") 146 | { 147 | set resName="blocks"_$zparent 148 | quit:aDirectory="" 0 149 | 150 | new $namespace 151 | znspace "%sys" 152 | 153 | OPEN 63:"^^"_aDirectory 154 | 155 | try { 156 | set tSC=..ReadBlocks(aAsTree, 3, "", "", 0, ResourceName) 157 | } catch ex { 158 | set tSC=ex.AsSystemError() 159 | } 160 | 161 | CLOSE 63 162 | $$$ThrowOnError(tSC) 163 | quit $$$OK 164 | } 165 | 166 | ClassMethod ReadBlocks(aAsTree As %Boolean = 0, aBlockId As %Integer = 3, aParentBlock = "", aGlobal As %String = "", aHasLong = 0, ResourceName As %String = "") 167 | { 168 | #define toInt(%bytes) ($a(%bytes,1)+($a(%bytes,2)*256)+($a(%bytes,3)*65536)+($a(%bytes,4)*16777216)) 169 | 170 | new $namespace 171 | znspace "%SYS" 172 | quit:aBlockId=0 0 173 | 174 | set blockSize=8192 175 | 176 | #dim error As %Exception.AbstractException = "" 177 | try { 178 | View aBlockId 179 | if aParentBlock'="" { 180 | set offset=$view(0,0,-4) 181 | set offset=$$$toInt(offset)+28 182 | do add(aParentBlock,aBlockId,aGlobal,offset) 183 | } 184 | set blockType=$view($Zutil(40,32,1),0,1) 185 | set nodes=0 186 | if blockType=8 { 187 | if aHasLong { 188 | For N=1:1 { 189 | Set X=$VIEW(N*2,-6) 190 | Quit:X="" 191 | set gdview=$ascii(X) 192 | if $listfind($listbuild(5,7,3),gdview) { 193 | set cnt=$piece(X,",",2) 194 | set blocks=$piece(X,",",4,*) 195 | for i=1:1:cnt { 196 | set nextBlock=$piece(X,",",3+i) 197 | do add(aBlockId,nextBlock,aGlobal,blockSize) 198 | } 199 | } 200 | } 201 | } 202 | } else { 203 | For N=1:1 { 204 | Set X=$VIEW(N-1*2+1,-6) 205 | Quit:X="" 206 | Set nextBlock=$VIEW(N*2,-5) 207 | if blockType=9 set aGlobal=X 208 | set haslong=0 209 | if $piece($view(N*2,-6),",",1) { 210 | set haslong=1 211 | } 212 | set nodes($increment(nodes))=$listbuild(nextBlock,aGlobal,haslong) 213 | } 214 | } 215 | 216 | for i=1:1:nodes { 217 | do ..ReadBlocks(aAsTree, $listget(nodes(i)), aBlockId, $listget(nodes(i),2), $listget(nodes(i),3), ResourceName) 218 | } 219 | } catch error { 220 | 221 | } 222 | #; finally 223 | 224 | if $isobject(error) Throw error 225 | 226 | quit $$$OK 227 | add(parentBlock,blockId,global,offset) 228 | set data=$listbuild(parentBlock,blockId) 229 | if 'aAsTree set data=data_$listbuild(global,$j(offset/blockSize*100,0,0)) 230 | do $system.Event.Signal(ResourceName, data) 231 | } 232 | 233 | /// Reads up to len characters from the client. 234 | /// If the call is successful the status (sc) will be returned as $$$OK, otherwise an error code of $$$CSPWebSocketTimeout 235 | /// indicates a timeout and $$$CSPWebSocketClosed indicates that the client has terminated the WebSocket. 236 | Method Read(ByRef len As %Integer = 32656, ByRef sc As %Status, timeout As %Integer = 86400) As %String 237 | { 238 | Set json = ##super(len, .sc, timeout) 239 | Do:$$$ISERR(sc) ..EndServer() 240 | quit:json="" "" 241 | try { 242 | set obj = ##class(%DynamicObject).%FromJSON(json) 243 | } catch ex { 244 | set sc = ex.AsStatus() 245 | set obj = "" 246 | } 247 | Quit obj 248 | } 249 | 250 | Method Write(eventName As %String = "", data As %String) As %Status 251 | { 252 | if '$d(data) { 253 | quit ##super(eventName) 254 | } 255 | set response = ##class(%DynamicObject).%New() 256 | set response.event = eventName 257 | set response.data = data 258 | 259 | set json = response.%ToJSON() 260 | quit ##super(json) 261 | } 262 | 263 | } 264 | -------------------------------------------------------------------------------- /server/src/DevInstaller.cls: -------------------------------------------------------------------------------- 1 | Class Blocks.DevInstaller 2 | { 3 | 4 | XData setup [ XMLNamespace = INSTALLER ] 5 | { 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | } 42 | 43 | ClassMethod setupWithVars(pRootDir As %String = "") 44 | { 45 | set vars("FOLDER") = ##class(%File).NormalizeDirectory(pRootDir) 46 | quit ..setup(.vars) 47 | } 48 | 49 | ClassMethod setup(ByRef pVars, pLogLevel As %Integer = 3, pInstaller As %Installer.Installer, pLogger As %Installer.AbstractLogger) As %Status [ CodeMode = objectgenerator ] 50 | { 51 | do %code.WriteLine($c(9)_"set pVars(""CURRENTCLASS"")="""_%classname_"""") 52 | do %code.WriteLine($c(9)_"set pVars(""CURRENTNS"")=$namespace") 53 | quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "setup") 54 | } 55 | 56 | ClassMethod SetDispatchClass(pCSPName As %String = "", pDispatchClass As %String = "") As %Status 57 | { 58 | new $namespace 59 | znspace "%SYS" 60 | set props("DispatchClass")=pDispatchClass 61 | set props("Recurse")=1 62 | set props("AutheEnabled") = 64 63 | set props("MatchRoles") = ":%ALL" 64 | d ##class(Security.Applications).Modify(pCSPName,.props) 65 | quit $$$OK 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions", 9 | "safari >= 7" 10 | ] 11 | } 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "error", 10 | 2 11 | ], 12 | "linebreak-style": [ 13 | "error", 14 | "unix" 15 | ], 16 | "quotes": [ 17 | "error", 18 | "single" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-slim 2 | 3 | # RUN apk update && apk add git 4 | 5 | WORKDIR /opt/app 6 | 7 | COPY package.json package-lock.json ./ 8 | 9 | RUN npm install 10 | 11 | ENV PATH="$PATH:./node_modules/.bin" 12 | 13 | VOLUME /opt/app/node_modules 14 | VOLUME /opt/app/build 15 | 16 | CMD ["npm", "run", "serve:docker"] 17 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cache Blocks explorer 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
    16 |
17 |
18 | GlobalName 19 | blocks count / fill 20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | 33 |
34 |
35 |
fit
36 |
37 |
SVG
38 |
PNG
39 |
40 |
41 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /web/js/blocksViewer.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var joint = require('jointjs'); 3 | require('./joint.shapes.blocks') 4 | 5 | export class BlocksViewer { 6 | 7 | constructor(app, container) { 8 | this.app = app 9 | this.container = $(container) 10 | this.blocks = [] 11 | this.blockData = [] 12 | this.blocksParent = [] 13 | this.blocksLeft = [] 14 | 15 | this.ZOOM_DELTA = 0.2 16 | this.PAPER_SCALE = 1 17 | this.MIN_PAPER_SCALE = 0.2 18 | this.MAX_PAPER_SCALE = 2 19 | this.SYMBOL_12_WIDTH = 7 20 | 21 | this.init() 22 | } 23 | 24 | reset() { 25 | this.blocks = [] 26 | this.blockData = [] 27 | this.blocksParent = [] 28 | this.blocksLeft = [] 29 | 30 | this.graph.resetCells() 31 | } 32 | 33 | get(blockId) { 34 | var self = this 35 | 36 | if (this.blocks[blockId]) { 37 | var block = this.graph.getCell(this.blocks[blockId]) 38 | block.remove() 39 | } 40 | this.app.load('rest/block/' + blockId, { 41 | directory: this.app.database 42 | }, function (data) { 43 | self.add(blockId, data) 44 | }) 45 | } 46 | 47 | loadTree(data) { 48 | var self = this 49 | 50 | var addBlock = function (blockId, parentBlock) { 51 | var parent = (!parentBlock) ? null : self.blocks[parentBlock] || addBlock(parentBlock, null) 52 | 53 | var block = new joint.shapes.basic.Circle({ 54 | attrs: { 55 | text: { 56 | text: blockId 57 | } 58 | } 59 | }) 60 | 61 | self.blocks[blockId] = block 62 | self.graph.addCell(block) 63 | 64 | if (parent) { 65 | var link = new joint.dia.Link({ 66 | source: { 67 | id: parent.id 68 | }, 69 | target: { 70 | id: block.id 71 | } 72 | }) 73 | self.graph.addCell(link) 74 | } else { 75 | block.position(50, 50) 76 | } 77 | 78 | return block 79 | } 80 | 81 | _.each(data, function (parentBlock) { 82 | _.each(parentBlock.child, function (child) { 83 | addBlock(child, parentBlock.block) 84 | }) 85 | }) 86 | } 87 | 88 | add(blockId, blockData) { 89 | var self = this 90 | 91 | var block = new joint.shapes.blocks.Block({ 92 | position: { 93 | x: 0, 94 | y: 0 95 | }, 96 | blockData: blockData, 97 | SYMBOL_12_WIDTH: this.SYMBOL_12_WIDTH 98 | }) 99 | this.blocks[blockId] = block.id 100 | this.graph.addCell(block) 101 | 102 | var parentBlock = this.blocksParent[blockId] 103 | if (parentBlock) { 104 | var link = new joint.shapes.blocks.Link({ 105 | source: { 106 | id: this.blocks[parentBlock], 107 | port: 'node' + blockId 108 | }, 109 | target: { 110 | id: block.id, 111 | port: 'up' 112 | } 113 | }) 114 | self.graph.addCell(link) 115 | } 116 | 117 | var leftBlock = this.blocksLeft[blockId] 118 | if (leftBlock) { 119 | var link = new joint.shapes.blocks.Link({ 120 | source: { 121 | id: this.blocks[leftBlock], 122 | port: 'right' 123 | }, 124 | target: { 125 | id: block.id, 126 | port: 'left' 127 | } 128 | }) 129 | self.graph.addCell(link) 130 | } 131 | 132 | if (blockData.link && blockData.link > 0) { 133 | self.blocksLeft[blockData.link] = blockId 134 | } 135 | _.each(blockData.nodes, function (node, nodeId) { 136 | var nodeBlockId = node.blockId || 0 137 | if (nodeBlockId === 0) return 138 | 139 | self.blocksParent[node.blockId] = blockId 140 | }) 141 | } 142 | 143 | layout(options) { 144 | var rankSep = 60 145 | var nodeSep = 40 146 | var self = this 147 | options = options || {} 148 | 149 | if (!options.parent) { 150 | return 151 | } 152 | 153 | var parentBlock = this.graph.getCell(options.parent) 154 | var outPorts = parentBlock.get('outPorts') 155 | var links = this.graph.getConnectedLinks(parentBlock, { 156 | outbound: true 157 | }) 158 | var elements = {}, 159 | count = 0, 160 | fullHeight = 0 161 | _.each(links, function (link, i) { 162 | if (link.get('target').port === 'up') { 163 | var child = self.graph.getCell(link.get('target').id) 164 | fullHeight += child.getBBox().height 165 | count += 1 166 | var pos = outPorts.indexOf(link.get('source').port) 167 | elements[pos] = child 168 | } 169 | }) 170 | fullHeight += (count - 1) * nodeSep 171 | 172 | var left = parentBlock.getBBox().x + parentBlock.getBBox().width + rankSep 173 | var top = parentBlock.getBBox().y - (fullHeight - parentBlock.getBBox().height) / 2 174 | 175 | _.each(elements, function (block, i) { 176 | block.set('position', { 177 | x: left, 178 | y: top 179 | }) 180 | top += block.getBBox().height 181 | top += nodeSep 182 | }) 183 | } 184 | 185 | init() { 186 | let self = this 187 | let relP = { 188 | x: 0, 189 | y: 0, 190 | trigger: false 191 | } 192 | 193 | 194 | self.app.ws.bind('blocks_tree', function (data) { 195 | self.loadTree(data) 196 | }) 197 | 198 | this.graph = new joint.dia.Graph 199 | 200 | this.paper = new joint.dia.Paper({ 201 | el: this.container, 202 | width: this.container.outerWidth(), 203 | height: this.container.outerHeight(), 204 | gridSize: 10, 205 | perpendicularLinks: true, 206 | model: this.graph, 207 | elementView: joint.shapes.blocks.BlockView 208 | }) 209 | 210 | this.paper.toSVG = (callback) => { 211 | var svg = document.querySelector('svg'); 212 | var data = (new XMLSerializer()).serializeToString(svg) 213 | callback(data) 214 | } 215 | 216 | this.paper.toPNG = (callback) => { 217 | var svg = document.querySelector('svg'), 218 | data = (new XMLSerializer()).serializeToString(svg), 219 | width = this.paper.getContentBBox().width, 220 | height = this.paper.getContentBBox().height 221 | 222 | var canvas = document.createElement('canvas') 223 | var ctx = canvas.getContext('2d') 224 | canvas.setAttribute("width", width); 225 | canvas.setAttribute("height", height); 226 | 227 | var img = new Image() 228 | img.width = width 229 | img.height = height 230 | img.onload = () => { 231 | ctx.drawImage(img, 0, 0, img.width, img.height); 232 | 233 | canvas.toBlob((blob) => { 234 | console.log(blob) 235 | callback(blob) 236 | }) 237 | } 238 | img.src = 'data:image/svg+xml,' + data 239 | } 240 | 241 | this.graph.on('add', function (cell) { 242 | if (cell.isLink()) { 243 | self.layout({ 244 | parent: cell.get('source').id 245 | }) 246 | var block = self.graph.getCell(cell.get('target').id) 247 | var bbox = block.getBBox() 248 | // self.paperScroller.center(bbox.x + (bbox.width / 2), bbox.y) 249 | } else { 250 | self.zoom(self.MAX_PAPER_SCALE) 251 | self.zoomToFit() 252 | } 253 | }) 254 | 255 | this.graph.on('remove', function (cell, collection, options) { 256 | if (cell.isLink()) { 257 | var child = this.getCell(cell.get('target').id) 258 | if (child) { 259 | child.remove() 260 | } 261 | } else { 262 | var blockId = cell.get('blockData').blockId 263 | delete self.blocks[blockId] 264 | self.graph.removeCells(cell) 265 | self.layout() 266 | } 267 | }) 268 | 269 | this.paper.on("blank:pointerdown", function (e) { 270 | relP.x = e.pageX 271 | relP.y = e.pageY 272 | relP.trigger = true 273 | }) 274 | 275 | this.paper.on("blank:pointerup", function (e) { 276 | if (!relP.trigger) return 277 | self.paper.setOrigin( 278 | self.paper.options.origin.x + e.pageX - relP.x, 279 | self.paper.options.origin.y + e.pageY - relP.y 280 | ) 281 | relP.trigger = false 282 | }) 283 | 284 | var moveHandler = (e) => { 285 | if (!relP.trigger) { 286 | return 287 | } 288 | this.paper.setOrigin( 289 | this.paper.options.origin.x + e.pageX - relP.x, 290 | this.paper.options.origin.y + e.pageY - relP.y 291 | ); 292 | relP.x = e.pageX; relP.y = e.pageY; 293 | } 294 | 295 | this.container.on('mousemove', moveHandler) 296 | this.container.on('mousetouch', moveHandler) 297 | 298 | this.paper.on('cell:nodeclick', function (cell, evt, index) { 299 | var node = cell.model.blockData.nodes[index] 300 | if (node.blockId) { 301 | self.get(node.blockId) 302 | } 303 | }) 304 | 305 | this.app.elements.zoomInBtn.addEventListener('click', function () { 306 | self.zoom(self.ZOOM_DELTA) 307 | }) 308 | this.app.elements.zoomOutBtn.addEventListener('click', function () { 309 | self.zoom(-self.ZOOM_DELTA) 310 | }) 311 | this.app.elements.zoomToFitBtn.addEventListener('click', function () { 312 | self.zoomToFit() 313 | // self.zoom(self.MAX_PAPER_SCALE) 314 | }) 315 | this.app.elements.downloadSVGBtn.addEventListener('click', () => { 316 | 317 | this.paper.toSVG((svg) => { 318 | svg = new Blob([svg], { 319 | type: 'image/svg+xml' 320 | }) 321 | var link = $('') 322 | .hide() 323 | .attr('href', URL.createObjectURL(svg)) 324 | .attr('download', 'blocks.svg') 325 | .text('download') 326 | link.get(0).click() 327 | setTimeout(function () { 328 | link.remove() 329 | }, 10) 330 | }) 331 | }) 332 | this.app.elements.downloadPNGBtn.addEventListener('click', function () { 333 | self.paper.toPNG((png) => { 334 | var link = $('') 335 | .hide() 336 | .attr('href', URL.createObjectURL(png)) 337 | .attr('download', 'blocks.png') 338 | .text('download') 339 | link.get(0).click() 340 | setTimeout(function () { 341 | link.remove() 342 | }, 10) 343 | }) 344 | }) 345 | 346 | this.SYMBOL_12_WIDTH = (function () { 347 | var e = document.createElementNS('http://www.w3.org/2000/svg', 'text'), 348 | s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), 349 | w 350 | s.appendChild(e) 351 | s.setAttribute('xmlns', 'http://www.w3.org/2000/svg') 352 | s.setAttribute('version', '1.1') 353 | e.setAttribute('font-family', 'monospace') 354 | e.setAttribute('font-size', '12') 355 | e.textContent = 'aBcDeFgGhH' 356 | document.body.appendChild(s) 357 | w = e.getBBox().width / 10 358 | s.parentNode.removeChild(s) 359 | return w 360 | })() 361 | 362 | this.initWS() 363 | } 364 | 365 | initWS() { 366 | var self = this 367 | 368 | this.app.ws.onmessage = function () { 369 | if (self.app.viewType.is(':checked')) { 370 | return 371 | } 372 | self.wsmessage.apply(self, arguments) 373 | } 374 | } 375 | 376 | wsmessage(event) { 377 | var self = this 378 | try { 379 | var data = JSON.parse(event.data) 380 | $.each(data, function (i, glob) { 381 | 382 | }) 383 | } catch (ex) { 384 | 385 | } 386 | } 387 | 388 | zoom(delta) { 389 | var scaleOld = this.PAPER_SCALE 390 | var scaleDelta; 391 | 392 | var sw = this.container.width(), 393 | sh = this.container.height(), 394 | side = delta > 0 ? 1 : -1, 395 | ox = this.paper.options.origin.x, 396 | oy = this.paper.options.origin.y; 397 | if (typeof delta === "number") { 398 | this.PAPER_SCALE += delta * Math.min( 399 | 0.3, 400 | Math.abs(this.PAPER_SCALE - (delta < 0 ? this.MIN_PAPER_SCALE : this.MAX_PAPER_SCALE)) / 2 401 | ); 402 | } else { this.PAPER_SCALE = 1; } 403 | this.paper.scale(this.PAPER_SCALE, this.PAPER_SCALE); 404 | scaleDelta = side * 405 | (side > 0 ? this.PAPER_SCALE / scaleOld - 1 : (scaleOld - this.PAPER_SCALE) / scaleOld); 406 | this.paper.setOrigin( 407 | ox - (sw / 2 - ox) * scaleDelta, 408 | oy - (sh / 2 - oy) * scaleDelta 409 | ); 410 | } 411 | 412 | zoomToFit() { 413 | const padding = 20 414 | this.paper.scale(1, 1) 415 | 416 | var sw = this.container.width() - (padding * 2), 417 | sh = this.container.height() - (padding * 2), 418 | ox = this.paper.options.origin.x, 419 | oy = this.paper.options.origin.y, 420 | bbox = this.paper.getContentBBox(); 421 | 422 | this.PAPER_SCALE = Math.max( 423 | this.MIN_PAPER_SCALE, 424 | Math.min( 425 | this.MAX_PAPER_SCALE, 426 | Math.min( 427 | sw / bbox.width, 428 | sh / bbox.height, 429 | ) 430 | ) 431 | ) 432 | 433 | this.paper.scale(this.PAPER_SCALE, this.PAPER_SCALE) 434 | bbox = this.paper.getContentBBox() 435 | this.paper.setOrigin( 436 | (sw - bbox.width) / 2 + padding, 437 | (sh - bbox.height) / 2 + padding 438 | ) 439 | 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /web/js/joint.shapes.blocks.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var joint = require('jointjs'); 3 | var V = joint.V 4 | 5 | joint.shapes.blocks = {} 6 | 7 | joint.shapes.blocks.Block = joint.shapes.devs.Model.extend(_.extend({}, joint.shapes.basic.PortsModelInterface, { 8 | 9 | markup: [ 10 | '', 11 | '', 12 | '', 13 | '', 14 | '', 15 | '', 16 | '', 17 | '', 18 | '', 19 | '', 20 | '', 21 | '', 22 | '', 23 | '' 24 | ].join(''), 25 | portMarkup: [ 26 | '', 27 | '', 28 | '' 29 | ].join(''), 30 | 31 | defaults: joint.util.deepSupplement({ 32 | 33 | type: 'blocks.Block', 34 | MIN_WIDTH: 100, 35 | size: { 36 | width: 150, 37 | height: 200 38 | }, 39 | 40 | extPorts: ['up', 'left', 'right'], 41 | outPorts: [], 42 | 43 | attrs: { 44 | '.': { 45 | magnet: true 46 | }, 47 | rect: { 48 | stroke: 'black', 49 | width: 150 50 | }, 51 | text: { 52 | 'fill': 'black', 53 | 'font-size': 12, 54 | 'pointer-events': 'none' 55 | }, 56 | '.blocks-id-rect': {}, 57 | '.blocks-id-text': { 58 | 'ref': '.blocks-id-rect', 59 | 'ref-x': 5, 60 | 'ref-y': 3 61 | }, 62 | '.blocks-info-rect': {}, 63 | '.blocks-info-text': { 64 | 'ref': '.blocks-info-rect', 65 | 'ref-x': 5, 66 | 'ref-y': 3 67 | }, 68 | '.blocks-nodes-label': { 69 | ref: '.blocks-nodes-rect', 70 | 'font-size': 10 71 | }, 72 | '.blocks-nodes-rect': {}, 73 | '.blocks-nodes-text': { 74 | 'ref': '.blocks-nodes-rect', 75 | 'ref-y': 2, 76 | 'ref-x': 8 77 | }, 78 | '.port-body': { 79 | r: 4, 80 | magnet: true, 81 | stroke: '#000000' 82 | } 83 | }, 84 | 85 | dataBlock: {} 86 | 87 | }, joint.shapes.basic.Generic.prototype.defaults), 88 | 89 | initialize: function () { 90 | var self = this 91 | var SYMBOL_12_WIDTH = this.get('SYMBOL_12_WIDTH') || 6.6 92 | this.blockData = this.get('blockData') 93 | 94 | this.rects = [{ 95 | type: 'id', 96 | text: [ 97 | 'Block # ' + this.blockData.blockId 98 | ] 99 | }, { 100 | type: 'info', 101 | text: [ 102 | 'Type: ' + this.blockData.type + ' ' + 103 | this.blockData.typename, 104 | 'Link: ' + (this.blockData.link || 0), 105 | 'Offset: ' + (this.blockData.offset || 0) 106 | ] 107 | }] 108 | 109 | var nodesRect = { 110 | type: 'nodes', 111 | text: [] 112 | } 113 | var outPorts = [] 114 | _.each(this.blockData.nodes, function (node, i) { 115 | var text = (nodesRect.text.length + 1) + ') ' + node.print 116 | if (node.blockId) { 117 | text += ' - ' + node.blockId 118 | outPorts.push('node' + node.blockId) 119 | } 120 | nodesRect.text.push(text) 121 | }) 122 | nodesRect.text.push('') 123 | 124 | this.get('attrs').blocktype = this.blockData.type 125 | 126 | this.rects.push(nodesRect) 127 | this.set('outPorts', outPorts) 128 | 129 | this.defaults.size.width = Math.max(this.defaults.MIN_WIDTH, 150) 130 | _.each(this.rects, function (rect) { 131 | rect.text.forEach(function (s) { 132 | var t = s.length * SYMBOL_12_WIDTH + 20 133 | if (t > self.defaults.size.width) { 134 | self.defaults.size.width = t 135 | } 136 | }) 137 | }) 138 | 139 | switch (this.blockData.type) { 140 | case 9: 141 | this.fillColor = 'coral' 142 | break 143 | case 70: 144 | this.fillColor = 'moccasin' 145 | break 146 | case 8: 147 | this.fillColor = 'palegreen' 148 | break 149 | case 6: 150 | this.fillColor = 'honeydew' 151 | break 152 | case 24: 153 | case 66: 154 | default: 155 | this.fillColor = 'powderblue' 156 | } 157 | 158 | this.updateRectangles() 159 | 160 | this.updatePortsAttrs() 161 | this.on('change:allPorts', this.updatePortsAttrs, this) 162 | 163 | this.on('change:name change:attributes change:methods', function () { 164 | this.updateRectangles() 165 | this.trigger('blocks-update') 166 | }, this) 167 | 168 | joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments) 169 | }, 170 | 171 | updateRectangles: function () { 172 | var self = this 173 | var attrs = this.get('attrs') 174 | 175 | var offsetY = 0 176 | 177 | _.each(this.rects, function (rect) { 178 | var lines = _.isArray(rect.text) ? rect.text : [{ 179 | text: rect.text 180 | }] 181 | 182 | var rectHeight = (lines.length - (rect.type === 'nodes' ? 1 : 0)) * 12 + (lines.length ? 4 : 0) 183 | var rectText = attrs['.blocks-' + rect.type + '-text'] 184 | var rectRect = attrs['.blocks-' + rect.type + '-rect'] 185 | 186 | if (rect.type === 'id' || rect.type === 'info') { 187 | rectRect.fill = self.fillColor 188 | } 189 | 190 | rectText.text = lines.join('\n') 191 | rectRect.transform = 'translate(0,' + offsetY + ')' 192 | 193 | rectRect.height = rectHeight 194 | offsetY += rectHeight 195 | }) 196 | 197 | this.attributes.size.height = offsetY 198 | this.attributes.size.width = this.defaults.size.width 199 | this.attributes.attrs.rect.width = this.defaults.size.width 200 | }, 201 | getPortAttrs: function (portName, index, total, selector, type) { 202 | var attrs = {} 203 | 204 | var portClass = 'port' + index 205 | var portSelector = selector + '>.' + portClass 206 | var portBodySelector = portSelector + '>.port-body' 207 | 208 | attrs[portBodySelector] = { 209 | port: { 210 | id: portName || _.uniqueId(type), 211 | type: type 212 | } 213 | } 214 | 215 | var ref, 216 | refX, 217 | refY, 218 | refDX, 219 | refDY 220 | if (portName === 'up') { 221 | ref = '.blocks-id-rect' 222 | refY = 8 223 | } else if (portName === 'left') { 224 | ref = '.blocks-id-rect' 225 | refX = 0.5 226 | refY = 0 227 | } else if (portName === 'right') { 228 | ref = '.block' 229 | refX = 0.5 230 | refDY = 0 231 | } else { 232 | ref = '.blocks-nodes-rect' 233 | refY = (index + 0.5) * (1 / total) 234 | refDX = 0 235 | } 236 | 237 | attrs[portSelector] = { 238 | ref: ref, 239 | 'ref-x': refX, 240 | 'ref-y': refY, 241 | 'ref-dx': refDX, 242 | 'ref-dy': refDY 243 | } 244 | 245 | return attrs 246 | }, 247 | updatePortsAttrs: function () { 248 | var self = this; 249 | var currAttrs = this.get('attrs') 250 | this._portSelectors = this._portSelectors || []; 251 | this._portSelectors.forEach((selector) => { 252 | if (currAttrs[selector]) delete currAttrs[selector] 253 | }) 254 | 255 | this._portSelectors = [] 256 | 257 | var attrs = {} 258 | 259 | this.get('extPorts').forEach((portName, index, ports) => { 260 | var portAttributes = self.getPortAttrs(portName, index, 1, '.' + portName + 'Port', 'ext') 261 | self._portSelectors = self._portSelectors.concat(_.keys(portAttributes)) 262 | _.extend(attrs, portAttributes) 263 | }) 264 | 265 | this.get('outPorts').forEach((portName, index, ports) => { 266 | var portAttributes = this.getPortAttrs(portName, index, ports.length, '.outPorts', 'out') 267 | this._portSelectors = this._portSelectors.concat(_.keys(portAttributes)) 268 | _.extend(attrs, portAttributes) 269 | }) 270 | 271 | this.attr(attrs, { 272 | silent: true 273 | }) 274 | this.processPorts() 275 | this.trigger('process:ports') 276 | } 277 | })) 278 | 279 | joint.shapes.blocks.Link = joint.dia.Link.extend({ 280 | 281 | // defaults: { 282 | // type: 'blocks.Link', 283 | // smooth: true, 284 | // attrs: { 285 | // '.connection': { 286 | // 'stroke-width': 1 287 | // }, 288 | // '.marker-target': { 289 | // d: 'M7,0L0,4L7,7L5,4z', 290 | // fill: 'black' 291 | // } 292 | // }, 293 | // router: { 294 | // name: 'manhattan' 295 | // }, 296 | // connector: { 297 | // name: 'rounded' 298 | // } 299 | // } 300 | }) 301 | 302 | joint.shapes.blocks.BlockView = joint.dia.ElementView.extend(_.extend({}, joint.shapes.basic.PortsViewInterface, { 303 | renderPorts: function () { 304 | 305 | var $extPorts = this.$('.extPorts').empty() 306 | var $outPorts = this.$('.outPorts').empty() 307 | 308 | var portTemplate = _.template(this.model.portMarkup) 309 | 310 | _.each(_.filter(this.model.ports, function (p) { 311 | return p.type === 'ext' 312 | }), function (port, index) { 313 | 314 | var $port = $extPorts.filter('.' + port.id + 'Port') 315 | $port.append(V(portTemplate({ 316 | id: index, 317 | port: port 318 | })).node) 319 | }) 320 | 321 | _.each(_.filter(this.model.ports, function (p) { 322 | return p.type === 'out' 323 | }), function (port, index) { 324 | 325 | $outPorts.append(V(portTemplate({ 326 | id: index, 327 | port: port 328 | })).node) 329 | }) 330 | }, 331 | 332 | pointerclick: function (evt, x, y) { 333 | if ($(evt.target).parent().is('.blocks-nodes-text')) { 334 | var index = $(evt.target).index() 335 | this.notify('cell:nodeclick', evt, index) 336 | } 337 | } 338 | })) 339 | 340 | if (typeof exports === 'object') { 341 | module.exports = joint.shapes.blocks 342 | } 343 | 344 | -------------------------------------------------------------------------------- /web/js/main.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | import {BlocksViewer} from './blocksViewer'; 3 | import {FancyWebSocket} from './wsEventDispatcher'; 4 | var MapViewer = require('./mapViewer'); 5 | require('../styles/main.scss') 6 | 7 | var app 8 | $(function () { 9 | app = new App() 10 | window.app = app; 11 | 12 | }) 13 | 14 | var App = function () { 15 | this.databaseSelect = $('#databaseSelect') 16 | this.viewType = $('#viewType') 17 | this.database = null 18 | this.blockInfo = $('#header .blockInfo') 19 | 20 | this.elements = { 21 | viewType: $('#viewType').get(0), 22 | blocksViewer: $('#blocksViewer').get(0), 23 | zoomInBtn: $('#btnZoomIn').get(0), 24 | zoomOutBtn: $('#btnZoomOut').get(0), 25 | zoomToFitBtn: $('#btnZoomToFit').get(0), 26 | downloadSVGBtn: $('#btnDownloadSVG').get(0), 27 | downloadPNGBtn: $('#btnDownloadPNG').get(0), 28 | mapViewer: $('#map canvas').get(0) 29 | } 30 | 31 | var wsUrl = ((window.location.protocol == "https:") ? "wss:" : "ws:" + "//" + window.location.host) 32 | wsUrl += window.location.pathname + 'websocket' 33 | this.ws = new FancyWebSocket(wsUrl) 34 | 35 | this.ws.bind('error', function (data) { 36 | console.log(data) 37 | }) 38 | 39 | this.blocksViewer = new BlocksViewer(this, this.elements.blocksViewer) 40 | 41 | this.mapViewer = new MapViewer(this, this.elements.mapViewer) 42 | 43 | this.init() 44 | 45 | return this 46 | } 47 | 48 | App.prototype.checkWSState = function () { 49 | var self = this 50 | 51 | 52 | setTimeout(function () { 53 | self.checkWSState() 54 | }, 1000) 55 | } 56 | 57 | App.prototype.load = function (url, data, callback) { 58 | var self = this 59 | 60 | $.ajax({ 61 | url: url, 62 | type: data ? 'POST' : 'GET', 63 | data: data, 64 | dataType: 'json', 65 | complete: function () { 66 | 67 | }, 68 | success: function (data, status) { 69 | callback.apply(self, [data, status]) 70 | } 71 | }) 72 | } 73 | 74 | App.prototype.updateDatabases = function (data) { 75 | var self = this 76 | 77 | this.databaseSelect.find('option').remove() 78 | 79 | $('